2019-11-29 06:59:40 -05:00
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
2017-08-02 04:36:54 -04:00
package app
import (
2020-01-15 13:38:55 -05:00
"encoding/base64"
"fmt"
"io"
2017-08-02 04:36:54 -04:00
"net/http"
2017-09-01 09:00:27 -04:00
"os"
2018-07-26 08:45:26 -04:00
"path/filepath"
2020-10-05 13:01:33 -04:00
"runtime"
2025-08-27 16:07:31 -04:00
"slices"
2019-09-17 15:02:26 -04:00
"sort"
2018-07-16 16:56:55 -04:00
"strings"
2020-02-17 17:30:48 -05:00
"sync"
2017-08-02 04:36:54 -04:00
2023-08-21 05:29:12 -04:00
"github.com/blang/semver/v4"
2021-01-07 12:12:43 -05:00
svg "github.com/h2non/go-is-svg"
"github.com/pkg/errors"
2026-01-20 09:46:34 -05:00
"google.golang.org/grpc"
2021-01-07 12:12:43 -05:00
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
2026-01-20 09:46:34 -05:00
apiserver "github.com/mattermost/mattermost/server/public/pluginapi/grpc/server"
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/public/shared/mlog"
2023-09-05 03:47:30 -04:00
"github.com/mattermost/mattermost/server/public/shared/request"
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
"github.com/mattermost/mattermost/server/v8/platform/services/marketplace"
2017-08-02 04:36:54 -04:00
)
2023-08-08 17:29:57 -04:00
// prepackagedPluginsDir is the hard-coded folder name where prepackaged plugins are bundled
// alongside the server.
2020-01-15 13:38:55 -05:00
const prepackagedPluginsDir = "prepackaged_plugins"
2023-08-08 17:29:57 -04:00
// pluginSignaturePath tracks the path to the plugin bundle and signature for the given plugin.
2019-11-18 19:02:41 -05:00
type pluginSignaturePath struct {
2021-02-25 14:22:27 -05:00
pluginID string
2023-08-08 17:29:57 -04:00
bundlePath string
2019-11-18 19:02:41 -05:00
signaturePath string
}
2018-11-20 08:52:51 -05:00
// GetPluginsEnvironment returns the plugin environment for use if plugins are enabled and
// initialized.
//
// To get the plugins environment when the plugins are disabled, manually acquire the plugins
// lock instead.
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) GetPluginsEnvironment ( ) * plugin . Environment {
if ! * ch . cfgSvc . Config ( ) . PluginSettings . Enable {
2018-11-20 08:52:51 -05:00
return nil
}
2022-12-21 14:10:26 -05:00
ch . pluginsLock . RLock ( )
defer ch . pluginsLock . RUnlock ( )
2020-06-12 07:43:50 -04:00
2022-12-21 14:10:26 -05:00
return ch . pluginsEnvironment
2020-06-12 07:43:50 -04:00
}
2018-11-20 08:52:51 -05:00
2020-06-12 07:43:50 -04:00
// GetPluginsEnvironment returns the plugin environment for use if plugins are enabled and
// initialized.
//
// To get the plugins environment when the plugins are disabled, manually acquire the plugins
// lock instead.
func ( a * App ) GetPluginsEnvironment ( ) * plugin . Environment {
2022-12-21 14:10:26 -05:00
return a . ch . GetPluginsEnvironment ( )
2018-11-20 08:52:51 -05:00
}
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) SetPluginsEnvironment ( pluginsEnvironment * plugin . Environment ) {
ch . pluginsLock . Lock ( )
defer ch . pluginsLock . Unlock ( )
2018-11-20 08:52:51 -05:00
2022-12-21 14:10:26 -05:00
ch . pluginsEnvironment = pluginsEnvironment
ch . srv . Platform ( ) . SetPluginsEnvironment ( ch )
2021-05-11 06:00:44 -04:00
}
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) syncPluginsActiveState ( ) {
2019-10-09 17:19:01 -04:00
// Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment.
2022-12-21 14:10:26 -05:00
ch . pluginsLock . RLock ( )
pluginsEnvironment := ch . pluginsEnvironment
ch . pluginsLock . RUnlock ( )
2018-11-20 08:52:51 -05:00
if pluginsEnvironment == nil {
2017-09-01 09:00:27 -04:00
return
}
2022-12-21 14:10:26 -05:00
config := ch . cfgSvc . Config ( ) . PluginSettings
2018-05-23 14:26:35 -04:00
2018-06-25 15:33:13 -04:00
if * config . Enable {
2018-11-20 08:52:51 -05:00
availablePlugins , err := pluginsEnvironment . Available ( )
2018-06-25 15:33:13 -04:00
if err != nil {
2022-12-21 14:10:26 -05:00
ch . srv . Log ( ) . Error ( "Unable to get available plugins" , mlog . Err ( err ) )
2018-06-25 15:33:13 -04:00
return
2017-10-25 08:17:17 -04:00
}
2020-08-27 09:22:20 -04:00
// Determine which plugins need to be activated or deactivated.
disabledPlugins := [ ] * model . BundleInfo { }
enabledPlugins := [ ] * model . BundleInfo { }
2019-06-13 14:52:50 -04:00
for _ , plugin := range availablePlugins {
2021-02-25 14:22:27 -05:00
pluginID := plugin . Manifest . Id
2018-06-25 15:33:13 -04:00
pluginEnabled := false
2021-02-25 14:22:27 -05:00
if state , ok := config . PluginStates [ pluginID ] ; ok {
2018-06-25 15:33:13 -04:00
pluginEnabled = state . Enable
2017-10-25 08:17:17 -04:00
}
2022-12-21 14:10:26 -05:00
if hasOverride , value := ch . getPluginStateOverride ( pluginID ) ; hasOverride {
2022-05-10 13:29:48 -04:00
pluginEnabled = value
2021-03-29 09:53:31 -04:00
}
2020-08-27 09:22:20 -04:00
if pluginEnabled {
enabledPlugins = append ( enabledPlugins , plugin )
} else {
disabledPlugins = append ( disabledPlugins , plugin )
}
}
// Concurrently activate/deactivate each plugin appropriately.
var wg sync . WaitGroup
// Deactivate any plugins that have been disabled.
for _ , plugin := range disabledPlugins {
wg . Add ( 1 )
go func ( plugin * model . BundleInfo ) {
defer wg . Done ( )
deactivated := pluginsEnvironment . Deactivate ( plugin . Manifest . Id )
2018-07-31 16:29:52 -04:00
if deactivated && plugin . Manifest . HasClient ( ) {
2022-09-02 06:17:22 -04:00
message := model . NewWebSocketEvent ( model . WebsocketEventPluginDisabled , "" , "" , "" , nil , "" )
2018-07-31 16:29:52 -04:00
message . Add ( "manifest" , plugin . Manifest . ClientManifest ( ) )
2022-12-21 14:10:26 -05:00
ch . srv . platform . Publish ( message )
2018-07-31 16:29:52 -04:00
}
2020-08-27 09:22:20 -04:00
} ( plugin )
2017-12-08 14:55:41 -05:00
}
2018-05-23 14:26:35 -04:00
2018-06-25 15:33:13 -04:00
// Activate any plugins that have been enabled
2020-08-27 09:22:20 -04:00
for _ , plugin := range enabledPlugins {
wg . Add ( 1 )
go func ( plugin * model . BundleInfo ) {
defer wg . Done ( )
2018-05-23 14:26:35 -04:00
2021-02-25 14:22:27 -05:00
pluginID := plugin . Manifest . Id
2023-08-08 17:29:57 -04:00
logger := ch . srv . Log ( ) . With ( mlog . String ( "plugin_id" , pluginID ) , mlog . String ( "bundle_path" , plugin . Path ) )
2021-02-25 14:22:27 -05:00
updatedManifest , activated , err := pluginsEnvironment . Activate ( pluginID )
2018-07-31 16:29:52 -04:00
if err != nil {
2023-08-08 17:29:57 -04:00
logger . Error ( "Unable to activate plugin" , mlog . Err ( err ) )
2020-08-27 09:22:20 -04:00
return
2018-07-31 16:29:52 -04:00
}
2019-08-22 15:17:47 -04:00
if activated {
// Notify all cluster clients if ready
2022-12-21 14:10:26 -05:00
if err := ch . notifyPluginEnabled ( updatedManifest ) ; err != nil {
2023-08-08 17:29:57 -04:00
logger . Error ( "Failed to notify cluster on plugin enable" , mlog . Err ( err ) )
2019-08-22 15:17:47 -04:00
}
2018-06-25 15:33:13 -04:00
}
2020-08-27 09:22:20 -04:00
} ( plugin )
2018-05-23 14:26:35 -04:00
}
2020-08-27 09:22:20 -04:00
wg . Wait ( )
2018-06-25 15:33:13 -04:00
} else { // If plugins are disabled, shutdown plugins.
2018-11-20 08:52:51 -05:00
pluginsEnvironment . Shutdown ( )
2017-09-01 09:00:27 -04:00
}
2017-12-08 14:55:41 -05:00
2022-12-21 14:10:26 -05:00
if err := ch . notifyPluginStatusesChanged ( ) ; err != nil {
2023-08-08 17:29:57 -04:00
ch . srv . Log ( ) . Warn ( "failed to notify plugin status changed" , mlog . Err ( err ) )
2018-05-23 14:26:35 -04:00
}
2017-09-01 09:00:27 -04:00
}
2025-09-10 09:11:32 -04:00
func ( a * App ) NewPluginAPI ( rctx request . CTX , manifest * model . Manifest ) plugin . API {
return NewPluginAPI ( a , rctx , manifest )
2021-05-11 06:00:44 -04:00
}
2025-09-10 09:11:32 -04:00
func ( a * App ) InitPlugins ( rctx request . CTX , pluginDir , webappPluginDir string ) {
a . ch . initPlugins ( rctx , pluginDir , webappPluginDir )
2017-11-30 15:55:44 -05:00
}
2025-09-10 09:11:32 -04:00
func ( ch * Channels ) initPlugins ( rctx request . CTX , pluginDir , webappPluginDir string ) {
2019-10-09 17:19:01 -04:00
// Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment.
2022-10-06 04:04:21 -04:00
defer func ( ) {
2022-12-21 14:10:26 -05:00
ch . srv . Platform ( ) . SetPluginsEnvironment ( ch )
2022-10-06 04:04:21 -04:00
} ( )
2022-12-21 14:10:26 -05:00
ch . pluginsLock . RLock ( )
pluginsEnvironment := ch . pluginsEnvironment
ch . pluginsLock . RUnlock ( )
if pluginsEnvironment != nil || ! * ch . cfgSvc . Config ( ) . PluginSettings . Enable {
ch . syncPluginsActiveState ( )
2021-12-17 01:39:10 -05:00
if pluginsEnvironment != nil {
2022-12-21 14:10:26 -05:00
pluginsEnvironment . TogglePluginHealthCheckJob ( * ch . cfgSvc . Config ( ) . PluginSettings . EnableHealthCheck )
2021-12-17 01:39:10 -05:00
}
2018-06-25 15:33:13 -04:00
return
2017-11-30 15:55:44 -05:00
}
2022-12-21 14:10:26 -05:00
ch . srv . Log ( ) . Info ( "Starting up plugins" )
2017-11-21 14:15:10 -05:00
2018-06-25 15:33:13 -04:00
if err := os . Mkdir ( pluginDir , 0744 ) ; err != nil && ! os . IsExist ( err ) {
2023-08-08 17:29:57 -04:00
ch . srv . Log ( ) . Error ( "Failed to start up plugins" , mlog . Err ( err ) )
2018-06-25 15:33:13 -04:00
return
2017-09-01 09:00:27 -04:00
}
2018-06-25 15:33:13 -04:00
if err := os . Mkdir ( webappPluginDir , 0744 ) ; err != nil && ! os . IsExist ( err ) {
2023-08-08 17:29:57 -04:00
ch . srv . Log ( ) . Error ( "Failed to start up plugins" , mlog . Err ( err ) )
2018-06-25 15:33:13 -04:00
return
2017-09-01 09:00:27 -04:00
}
2021-08-16 13:46:44 -04:00
newAPIFunc := func ( manifest * model . Manifest ) plugin . API {
2025-09-10 09:11:32 -04:00
return New ( ServerConnector ( ch ) ) . NewPluginAPI ( rctx , manifest )
2021-05-11 06:00:44 -04:00
}
2022-12-06 12:44:48 -05:00
env , err := plugin . NewEnvironment (
newAPIFunc ,
2022-12-21 14:10:26 -05:00
NewDriverImpl ( ch . srv ) ,
2022-12-06 12:44:48 -05:00
pluginDir ,
webappPluginDir ,
2022-12-21 14:10:26 -05:00
ch . srv . Log ( ) ,
ch . srv . GetMetrics ( ) ,
2022-12-06 12:44:48 -05:00
)
2018-11-20 08:52:51 -05:00
if err != nil {
2023-08-08 17:29:57 -04:00
ch . srv . Log ( ) . Error ( "Failed to start up plugins" , mlog . Err ( err ) )
2018-06-25 15:33:13 -04:00
return
2018-05-23 14:26:35 -04:00
}
2026-01-20 09:46:34 -05:00
// Set the API server registrar for Python plugins to call back to Go API.
// This breaks the import cycle between plugin and pluginapi/grpc/server packages.
env . SetAPIServerRegistrar ( func ( grpcServer * grpc . Server , api plugin . API ) {
apiserver . Register ( grpcServer , api )
} )
2022-12-21 14:10:26 -05:00
ch . pluginsLock . Lock ( )
ch . pluginsEnvironment = env
ch . pluginsLock . Unlock ( )
2018-05-23 14:26:35 -04:00
2022-12-21 14:10:26 -05:00
ch . pluginsEnvironment . TogglePluginHealthCheckJob ( * ch . cfgSvc . Config ( ) . PluginSettings . EnableHealthCheck )
2021-11-03 23:24:03 -04:00
2022-12-21 14:10:26 -05:00
if err := ch . syncPlugins ( ) ; err != nil {
2023-08-08 17:29:57 -04:00
ch . srv . Log ( ) . Error ( "Failed to sync plugins from the file store" , mlog . Err ( err ) )
2019-07-18 14:05:53 -04:00
}
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
if err := ch . processPrepackagedPlugins ( prepackagedPluginsDir ) ; err != nil {
ch . srv . Log ( ) . Error ( "Failed to process prepackaged plugins" , mlog . Err ( err ) )
2020-08-03 04:28:47 -04:00
}
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
ch . pluginClusterLeaderListenerID = ch . srv . AddClusterLeaderChangedListener ( func ( ) {
ch . persistTransitionallyPrepackagedPlugins ( )
} )
ch . persistTransitionallyPrepackagedPlugins ( )
2018-07-26 08:45:26 -04:00
2018-06-25 15:33:13 -04:00
// Sync plugin active state when config changes. Also notify plugins.
2022-12-21 14:10:26 -05:00
ch . pluginsLock . Lock ( )
ch . RemoveConfigListener ( ch . pluginConfigListenerID )
2025-03-17 09:22:07 -04:00
ch . pluginConfigListenerID = ch . AddConfigListener ( func ( oldCfg , newCfg * model . Config ) {
2021-04-30 10:16:57 -04:00
// If plugin status remains unchanged, only then run this.
// Because (*App).InitPlugins is already run as a config change hook.
2025-03-17 09:22:07 -04:00
if * oldCfg . PluginSettings . Enable == * newCfg . PluginSettings . Enable {
2022-12-21 14:10:26 -05:00
ch . syncPluginsActiveState ( )
2021-04-30 10:16:57 -04:00
}
2022-12-07 02:00:47 -05:00
2024-11-13 05:20:39 -05:00
ch . RunMultiHook ( func ( hooks plugin . Hooks , _ * model . Manifest ) bool {
2022-12-07 02:00:47 -05:00
if err := hooks . OnConfigurationChange ( ) ; err != nil {
2022-12-21 14:10:26 -05:00
ch . srv . Log ( ) . Error ( "Plugin OnConfigurationChange hook failed" , mlog . Err ( err ) )
2022-12-07 02:00:47 -05:00
}
return true
} , plugin . OnConfigurationChangeID )
2018-06-25 15:33:13 -04:00
} )
2022-12-21 14:10:26 -05:00
ch . pluginsLock . Unlock ( )
2017-09-01 09:00:27 -04:00
2022-12-21 14:10:26 -05:00
ch . syncPluginsActiveState ( )
2017-10-25 08:17:17 -04:00
}
2019-07-18 14:05:53 -04:00
// SyncPlugins synchronizes the plugins installed locally
// with the plugin bundles available in the file store.
func ( a * App ) SyncPlugins ( ) * model . AppError {
2022-12-21 14:10:26 -05:00
return a . ch . syncPlugins ( )
2021-05-11 06:00:44 -04:00
}
// SyncPlugins synchronizes the plugins installed locally
// with the plugin bundles available in the file store.
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) syncPlugins ( ) * model . AppError {
2023-08-08 17:29:57 -04:00
ch . srv . Log ( ) . Info ( "Syncing plugins from the file store" )
2019-07-18 14:05:53 -04:00
2022-12-21 14:10:26 -05:00
pluginsEnvironment := ch . GetPluginsEnvironment ( )
2019-07-18 14:05:53 -04:00
if pluginsEnvironment == nil {
return model . NewAppError ( "SyncPlugins" , "app.plugin.disabled.app_error" , nil , "" , http . StatusNotImplemented )
}
availablePlugins , err := pluginsEnvironment . Available ( )
if err != nil {
2022-08-18 05:01:37 -04:00
return model . NewAppError ( "SyncPlugins" , "app.plugin.sync.read_local_folder.app_error" , nil , "" , http . StatusInternalServerError ) . Wrap ( err )
2019-07-18 14:05:53 -04:00
}
2020-02-17 17:30:48 -05:00
var wg sync . WaitGroup
2019-07-18 14:05:53 -04:00
for _ , plugin := range availablePlugins {
2020-02-17 17:30:48 -05:00
wg . Add ( 1 )
go func ( pluginID string ) {
defer wg . Done ( )
2023-08-08 17:29:57 -04:00
logger := ch . srv . Log ( ) . With ( mlog . String ( "plugin_id" , pluginID ) )
2025-07-15 21:46:41 -04:00
logger . Info ( "Removing local installation of managed plugin before sync" )
if err := ch . removePluginLocally ( pluginID ) ; err != nil {
logger . Error ( "Failed to remove local installation of managed plugin before sync" , mlog . Err ( err ) )
2019-07-18 14:05:53 -04:00
}
2020-02-17 17:30:48 -05:00
} ( plugin . Manifest . Id )
2019-07-18 14:05:53 -04:00
}
2020-03-17 11:56:59 -04:00
wg . Wait ( )
2019-07-18 14:05:53 -04:00
// Install plugins from the file store.
2022-12-21 14:10:26 -05:00
pluginSignaturePathMap , appErr := ch . getPluginsFromFolder ( )
2019-07-18 14:05:53 -04:00
if appErr != nil {
2019-11-18 19:02:41 -05:00
return appErr
2019-07-18 14:05:53 -04:00
}
2019-12-12 12:45:55 -05:00
2023-08-09 10:54:41 -04:00
if len ( pluginSignaturePathMap ) == 0 {
ch . srv . Log ( ) . Info ( "No plugins to sync from the file store" )
return nil
}
2019-11-18 19:02:41 -05:00
for _ , plugin := range pluginSignaturePathMap {
2020-02-17 17:30:48 -05:00
wg . Add ( 1 )
go func ( plugin * pluginSignaturePath ) {
defer wg . Done ( )
2025-06-24 14:11:02 -04:00
logger := ch . srv . Log ( ) . With (
mlog . String ( "plugin_id" , plugin . pluginID ) ,
mlog . String ( "bundle_path" , plugin . bundlePath ) ,
mlog . String ( "signature_path" , plugin . signaturePath ) ,
)
2023-08-08 17:29:57 -04:00
bundle , appErr := ch . srv . fileReader ( plugin . bundlePath )
2019-11-18 19:02:41 -05:00
if appErr != nil {
2023-08-08 17:29:57 -04:00
logger . Error ( "Failed to open plugin bundle from file store." , mlog . Err ( appErr ) )
2020-02-17 17:30:48 -05:00
return
}
2023-08-08 17:29:57 -04:00
defer bundle . Close ( )
2020-02-17 17:30:48 -05:00
2022-12-21 14:10:26 -05:00
if * ch . cfgSvc . Config ( ) . PluginSettings . RequirePluginSignature {
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
signature , appErr := ch . srv . fileReader ( plugin . signaturePath )
2020-02-17 17:30:48 -05:00
if appErr != nil {
2023-08-08 17:29:57 -04:00
logger . Error ( "Failed to open plugin signature from file store." , mlog . Err ( appErr ) )
2020-02-17 17:30:48 -05:00
return
}
defer signature . Close ( )
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
2025-06-24 14:11:02 -04:00
if appErr = ch . verifyPlugin ( logger , bundle , signature ) ; appErr != nil {
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
logger . Error ( "Failed to validate plugin signature" , mlog . Err ( appErr ) )
return
}
2019-11-18 19:02:41 -05:00
}
2019-07-18 14:05:53 -04:00
2023-08-08 17:29:57 -04:00
logger . Info ( "Syncing plugin from file store" )
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
if _ , err := ch . installPluginLocally ( bundle , installPluginLocallyAlways ) ; err != nil && err . Id != "app.plugin.skip_installation.app_error" {
2023-08-08 17:29:57 -04:00
logger . Error ( "Failed to sync plugin from file store" , mlog . Err ( err ) )
2020-02-17 17:30:48 -05:00
}
} ( plugin )
2019-11-18 19:02:41 -05:00
}
2020-02-17 17:30:48 -05:00
wg . Wait ( )
2019-07-18 14:05:53 -04:00
return nil
}
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) ShutDownPlugins ( ) {
2021-11-03 23:24:03 -04:00
// Acquiring lock manually, as plugins might be disabled. See GetPluginsEnvironment.
2022-12-21 14:10:26 -05:00
ch . pluginsLock . RLock ( )
pluginsEnvironment := ch . pluginsEnvironment
ch . pluginsLock . RUnlock ( )
2018-11-20 08:52:51 -05:00
if pluginsEnvironment == nil {
2018-06-25 15:33:13 -04:00
return
2017-09-01 09:00:27 -04:00
}
2023-08-08 17:29:57 -04:00
ch . srv . Log ( ) . Info ( "Shutting down plugins" )
MM-8622: improved plugin error handling (#8692)
* don't report an error on plugin activation if already active
* improved plugin logging events
Log an error when a plugin's ServeHTTP fails, or when it unexpectedly
terminates.
Restart a plugin at most three times, allowing its failure to later
bubble up under the "failed to stay running" status.
* clarified plugin activation/deactivation
Avoid repeatedly activating when any configuration bit changes. Improved
logging.
* constrain plugin ids to ^[a-zA-Z0-9-_\.]+$ and enforce minimum length
Previously, the plugin id was used unsanitized to relocate the plugin
bundle, which allowed writing outside the `plugins/` directory by using
an `id` containing `../`.
Similarly, an empty string was accepted as an id and led to unexpected
error messages.
* remove plugins by manifest path, not id
If the id within the manifest ever diverges from the actual plugin
location, it becomes impossible to remove via the API. Instead, if the
plugin is found by id, remove the path containing the manifest.
* ignore plugins with nil manifests
If a plugin was detected, but had a manifest that couldn't be parsed, it
will be left nil but still be listed among the packages. Skip over these
in most cases to avoid segfaults.
* leverage mlog more effectively for plugins
* build issues
2018-05-01 10:34:12 -04:00
2018-11-20 08:52:51 -05:00
pluginsEnvironment . Shutdown ( )
2017-09-15 08:51:46 -04:00
2022-12-21 14:10:26 -05:00
ch . RemoveConfigListener ( ch . pluginConfigListenerID )
ch . pluginConfigListenerID = ""
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
ch . srv . RemoveClusterLeaderChangedListener ( ch . pluginClusterLeaderListenerID )
ch . pluginClusterLeaderListenerID = ""
2019-10-09 17:19:01 -04:00
// Acquiring lock manually before cleaning up PluginsEnvironment.
2022-12-21 14:10:26 -05:00
ch . pluginsLock . Lock ( )
defer ch . pluginsLock . Unlock ( )
if ch . pluginsEnvironment == pluginsEnvironment {
ch . pluginsEnvironment = nil
2019-10-09 17:19:01 -04:00
} else {
2023-08-08 17:29:57 -04:00
ch . srv . Log ( ) . Warn ( "Another PluginsEnvironment detected while shutting down plugins." )
2019-10-09 17:19:01 -04:00
}
2017-09-01 09:00:27 -04:00
}
2024-09-12 13:23:57 -04:00
func ( a * App ) getPluginManifests ( ) ( [ ] * model . Manifest , error ) {
pluginsEnvironment := a . GetPluginsEnvironment ( )
if pluginsEnvironment == nil {
return nil , model . NewAppError ( "GetPluginManifests" , "app.plugin.disabled.app_error" , nil , "" , http . StatusNotImplemented )
}
plugins , err := pluginsEnvironment . Available ( )
if err != nil {
return nil , errors . Wrap ( err , "failed to get list of available plugins" )
}
manifests := make ( [ ] * model . Manifest , len ( plugins ) )
for i := range plugins {
manifests [ i ] = plugins [ i ] . Manifest
}
return manifests , nil
}
2017-09-06 18:12:54 -04:00
func ( a * App ) GetActivePluginManifests ( ) ( [ ] * model . Manifest , * model . AppError ) {
2018-11-20 08:52:51 -05:00
pluginsEnvironment := a . GetPluginsEnvironment ( )
if pluginsEnvironment == nil {
2017-09-01 09:00:27 -04:00
return nil , model . NewAppError ( "GetActivePluginManifests" , "app.plugin.disabled.app_error" , nil , "" , http . StatusNotImplemented )
}
2018-11-20 08:52:51 -05:00
plugins := pluginsEnvironment . Active ( )
2017-09-01 09:00:27 -04:00
manifests := make ( [ ] * model . Manifest , len ( plugins ) )
for i , plugin := range plugins {
manifests [ i ] = plugin . Manifest
}
return manifests , nil
}
2018-05-23 14:26:35 -04:00
// EnablePlugin will set the config for an installed plugin to enabled, triggering asynchronous
// activation if inactive anywhere in the cluster.
2019-08-22 15:17:47 -04:00
// Notifies cluster peers through config change.
2017-10-25 08:17:17 -04:00
func ( a * App ) EnablePlugin ( id string ) * model . AppError {
2022-12-21 14:10:26 -05:00
return a . ch . enablePlugin ( id )
2021-05-11 06:00:44 -04:00
}
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) enablePlugin ( id string ) * model . AppError {
pluginsEnvironment := ch . GetPluginsEnvironment ( )
2018-11-20 08:52:51 -05:00
if pluginsEnvironment == nil {
2017-11-30 15:55:44 -05:00
return model . NewAppError ( "EnablePlugin" , "app.plugin.disabled.app_error" , nil , "" , http . StatusNotImplemented )
2017-10-25 08:17:17 -04:00
}
2020-01-15 13:38:55 -05:00
availablePlugins , err := pluginsEnvironment . Available ( )
2017-10-25 08:17:17 -04:00
if err != nil {
2022-08-18 05:01:37 -04:00
return model . NewAppError ( "EnablePlugin" , "app.plugin.config.app_error" , nil , "" , http . StatusInternalServerError ) . Wrap ( err )
2017-10-25 08:17:17 -04:00
}
2018-07-16 16:56:55 -04:00
id = strings . ToLower ( id )
2017-10-25 08:17:17 -04:00
var manifest * model . Manifest
2020-01-15 13:38:55 -05:00
for _ , p := range availablePlugins {
2017-10-25 08:17:17 -04:00
if p . Manifest . Id == id {
manifest = p . Manifest
break
}
}
if manifest == nil {
2019-11-26 06:36:51 -05:00
return model . NewAppError ( "EnablePlugin" , "app.plugin.not_installed.app_error" , nil , "" , http . StatusNotFound )
2017-10-25 08:17:17 -04:00
}
2022-12-21 14:10:26 -05:00
ch . cfgSvc . UpdateConfig ( func ( cfg * model . Config ) {
2017-10-31 10:39:31 -04:00
cfg . PluginSettings . PluginStates [ id ] = & model . PluginState { Enable : true }
} )
2017-10-25 08:17:17 -04:00
2019-08-22 15:17:47 -04:00
// This call will implicitly invoke SyncPluginsActiveState which will activate enabled plugins.
2022-12-21 14:10:26 -05:00
if _ , _ , err := ch . cfgSvc . SaveConfig ( ch . cfgSvc . Config ( ) , true ) ; err != nil {
2018-03-13 12:03:12 -04:00
if err . Id == "ent.cluster.save_config.error" {
return model . NewAppError ( "EnablePlugin" , "app.plugin.cluster.save_config.app_error" , nil , "" , http . StatusInternalServerError )
}
2022-08-18 05:01:37 -04:00
return model . NewAppError ( "EnablePlugin" , "app.plugin.config.app_error" , nil , "" , http . StatusInternalServerError ) . Wrap ( err )
2017-10-25 08:17:17 -04:00
}
return nil
}
// DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active.
2019-08-22 15:17:47 -04:00
// Notifies cluster peers through config change.
2017-10-25 08:17:17 -04:00
func ( a * App ) DisablePlugin ( id string ) * model . AppError {
2022-12-21 14:10:26 -05:00
appErr := a . ch . disablePlugin ( id )
2022-05-25 12:36:08 -04:00
if appErr != nil {
return appErr
}
return nil
2021-05-11 06:00:44 -04:00
}
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) disablePlugin ( id string ) * model . AppError {
pluginsEnvironment := ch . GetPluginsEnvironment ( )
2018-11-20 08:52:51 -05:00
if pluginsEnvironment == nil {
2017-11-30 15:55:44 -05:00
return model . NewAppError ( "DisablePlugin" , "app.plugin.disabled.app_error" , nil , "" , http . StatusNotImplemented )
2017-10-25 08:17:17 -04:00
}
2020-01-15 13:38:55 -05:00
availablePlugins , err := pluginsEnvironment . Available ( )
2017-10-25 08:17:17 -04:00
if err != nil {
2022-08-18 05:01:37 -04:00
return model . NewAppError ( "DisablePlugin" , "app.plugin.config.app_error" , nil , "" , http . StatusInternalServerError ) . Wrap ( err )
2017-10-25 08:17:17 -04:00
}
2018-07-16 16:56:55 -04:00
id = strings . ToLower ( id )
2017-10-25 08:17:17 -04:00
var manifest * model . Manifest
2020-01-15 13:38:55 -05:00
for _ , p := range availablePlugins {
2017-10-25 08:17:17 -04:00
if p . Manifest . Id == id {
manifest = p . Manifest
break
}
}
if manifest == nil {
2019-11-18 12:40:49 -05:00
return model . NewAppError ( "DisablePlugin" , "app.plugin.not_installed.app_error" , nil , "" , http . StatusNotFound )
2017-10-25 08:17:17 -04:00
}
2022-12-21 14:10:26 -05:00
ch . cfgSvc . UpdateConfig ( func ( cfg * model . Config ) {
2017-10-31 10:39:31 -04:00
cfg . PluginSettings . PluginStates [ id ] = & model . PluginState { Enable : false }
} )
2022-12-21 14:10:26 -05:00
ch . unregisterPluginCommands ( id )
2017-10-25 08:17:17 -04:00
2019-08-22 15:17:47 -04:00
// This call will implicitly invoke SyncPluginsActiveState which will deactivate disabled plugins.
2022-12-21 14:10:26 -05:00
if _ , _ , err := ch . cfgSvc . SaveConfig ( ch . cfgSvc . Config ( ) , true ) ; err != nil {
2022-08-18 05:01:37 -04:00
return model . NewAppError ( "DisablePlugin" , "app.plugin.config.app_error" , nil , "" , http . StatusInternalServerError ) . Wrap ( err )
2017-09-01 09:00:27 -04:00
}
2017-09-15 08:51:46 -04:00
return nil
2017-09-01 09:00:27 -04:00
}
2017-09-11 11:02:02 -04:00
2018-06-25 15:33:13 -04:00
func ( a * App ) GetPlugins ( ) ( * model . PluginsResponse , * model . AppError ) {
2018-11-20 08:52:51 -05:00
pluginsEnvironment := a . GetPluginsEnvironment ( )
if pluginsEnvironment == nil {
2018-06-25 15:33:13 -04:00
return nil , model . NewAppError ( "GetPlugins" , "app.plugin.disabled.app_error" , nil , "" , http . StatusNotImplemented )
2017-12-08 14:55:41 -05:00
}
2018-11-20 08:52:51 -05:00
availablePlugins , err := pluginsEnvironment . Available ( )
2018-06-25 15:33:13 -04:00
if err != nil {
2022-08-18 05:01:37 -04:00
return nil , model . NewAppError ( "GetPlugins" , "app.plugin.get_plugins.app_error" , nil , "" , http . StatusInternalServerError ) . Wrap ( err )
2017-12-08 14:55:41 -05:00
}
2018-06-25 15:33:13 -04:00
resp := & model . PluginsResponse { Active : [ ] * model . PluginInfo { } , Inactive : [ ] * model . PluginInfo { } }
for _ , plugin := range availablePlugins {
if plugin . Manifest == nil {
continue
2017-12-08 14:55:41 -05:00
}
2018-06-25 15:33:13 -04:00
info := & model . PluginInfo {
Manifest : * plugin . Manifest ,
2017-12-08 14:55:41 -05:00
}
2018-11-20 08:52:51 -05:00
if pluginsEnvironment . IsActive ( plugin . Manifest . Id ) {
2018-06-25 15:33:13 -04:00
resp . Active = append ( resp . Active , info )
} else {
resp . Inactive = append ( resp . Inactive , info )
2017-12-08 14:55:41 -05:00
}
}
2018-05-15 16:33:47 -04:00
2018-06-25 15:33:13 -04:00
return resp , nil
2018-05-15 16:33:47 -04:00
}
2019-08-22 15:17:47 -04:00
2020-01-15 13:38:55 -05:00
// GetMarketplacePlugins returns a list of plugins from the marketplace-server,
// and plugins that are installed locally.
2024-04-24 05:52:33 -04:00
func ( a * App ) GetMarketplacePlugins ( rctx request . CTX , filter * model . MarketplacePluginFilter ) ( [ ] * model . MarketplacePlugin , * model . AppError ) {
2020-01-15 13:38:55 -05:00
plugins := map [ string ] * model . MarketplacePlugin { }
2023-09-15 13:17:53 -04:00
if * a . Config ( ) . PluginSettings . EnableRemoteMarketplace && ! filter . LocalOnly {
2020-05-23 01:05:51 -04:00
p , appErr := a . getRemotePlugins ( )
2020-01-15 13:38:55 -05:00
if appErr != nil {
return nil , appErr
}
plugins = p
}
2023-01-26 11:08:05 -05:00
if ! filter . RemoteOnly {
2023-08-25 15:39:25 -04:00
appErr := a . mergePrepackagedPlugins ( plugins )
if appErr != nil {
return nil , appErr
2023-01-26 11:08:05 -05:00
}
2024-04-24 05:52:33 -04:00
appErr = a . mergeLocalPlugins ( rctx , plugins )
2021-01-31 03:17:46 -05:00
if appErr != nil {
return nil , appErr
}
2020-01-15 13:38:55 -05:00
}
// Filter plugins.
var result [ ] * model . MarketplacePlugin
for _ , p := range plugins {
if pluginMatchesFilter ( p . Manifest , filter . Filter ) {
result = append ( result , p )
}
}
// Sort result alphabetically.
sort . SliceStable ( result , func ( i , j int ) bool {
return strings . ToLower ( result [ i ] . Manifest . Name ) < strings . ToLower ( result [ j ] . Manifest . Name )
} )
return result , nil
}
// getPrepackagedPlugin returns a pre-packaged plugin.
2022-02-02 13:45:09 -05:00
//
// If version is empty, the first matching plugin is returned.
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) getPrepackagedPlugin ( pluginID , version string ) ( * plugin . PrepackagedPlugin , * model . AppError ) {
pluginsEnvironment := ch . GetPluginsEnvironment ( )
2020-01-15 13:38:55 -05:00
if pluginsEnvironment == nil {
return nil , model . NewAppError ( "getPrepackagedPlugin" , "app.plugin.config.app_error" , nil , "plugin environment is nil" , http . StatusInternalServerError )
}
prepackagedPlugins := pluginsEnvironment . PrepackagedPlugins ( )
for _ , p := range prepackagedPlugins {
2022-02-02 13:45:09 -05:00
if p . Manifest . Id == pluginID && ( version == "" || p . Manifest . Version == version ) {
2020-01-15 13:38:55 -05:00
return p , nil
}
}
return nil , model . NewAppError ( "getPrepackagedPlugin" , "app.plugin.marketplace_plugins.not_found.app_error" , nil , "" , http . StatusInternalServerError )
}
// getRemoteMarketplacePlugin returns plugin from marketplace-server.
2022-02-02 13:45:09 -05:00
//
// If version is empty, the latest compatible version is used.
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) getRemoteMarketplacePlugin ( pluginID , version string ) ( * model . BaseMarketplacePlugin , * model . AppError ) {
2019-11-18 19:02:41 -05:00
marketplaceClient , err := marketplace . NewClient (
2022-12-21 14:10:26 -05:00
* ch . cfgSvc . Config ( ) . PluginSettings . MarketplaceURL ,
ch . srv . HTTPService ( ) ,
2019-11-18 19:02:41 -05:00
)
if err != nil {
2022-08-18 05:01:37 -04:00
return nil , model . NewAppError ( "GetMarketplacePlugin" , "app.plugin.marketplace_client.app_error" , nil , "" , http . StatusInternalServerError ) . Wrap ( err )
2019-11-18 19:02:41 -05:00
}
2022-12-21 14:10:26 -05:00
filter := ch . getBaseMarketplaceFilter ( )
2021-02-25 14:22:27 -05:00
filter . PluginId = pluginID
2020-06-30 08:17:00 -04:00
2022-02-02 13:45:09 -05:00
var plugin * model . BaseMarketplacePlugin
if version != "" {
plugin , err = marketplaceClient . GetPlugin ( filter , version )
} else {
plugin , err = marketplaceClient . GetLatestPlugin ( filter )
}
2019-11-18 19:02:41 -05:00
if err != nil {
2022-08-18 05:01:37 -04:00
return nil , model . NewAppError ( "GetMarketplacePlugin" , "app.plugin.marketplace_plugins.not_found.app_error" , nil , "" , http . StatusInternalServerError ) . Wrap ( err )
2019-11-18 19:02:41 -05:00
}
2020-01-15 13:38:55 -05:00
2019-11-18 19:02:41 -05:00
return plugin , nil
}
2020-05-23 01:05:51 -04:00
func ( a * App ) getRemotePlugins ( ) ( map [ string ] * model . MarketplacePlugin , * model . AppError ) {
2020-01-15 13:38:55 -05:00
result := map [ string ] * model . MarketplacePlugin { }
2019-09-17 15:02:26 -04:00
pluginsEnvironment := a . GetPluginsEnvironment ( )
if pluginsEnvironment == nil {
2020-01-15 13:38:55 -05:00
return nil , model . NewAppError ( "getRemotePlugins" , "app.plugin.config.app_error" , nil , "" , http . StatusInternalServerError )
2019-09-17 15:02:26 -04:00
}
marketplaceClient , err := marketplace . NewClient (
2021-08-16 13:46:44 -04:00
* a . Config ( ) . PluginSettings . MarketplaceURL ,
2020-02-13 07:26:58 -05:00
a . HTTPService ( ) ,
2019-09-17 15:02:26 -04:00
)
if err != nil {
2022-08-18 05:01:37 -04:00
return nil , model . NewAppError ( "getRemotePlugins" , "app.plugin.marketplace_client.app_error" , nil , "" , http . StatusInternalServerError ) . Wrap ( err )
2019-09-17 15:02:26 -04:00
}
2020-06-30 08:17:00 -04:00
filter := a . getBaseMarketplaceFilter ( )
2019-09-17 15:02:26 -04:00
// Fetch all plugins from marketplace.
2020-06-30 08:17:00 -04:00
filter . PerPage = - 1
2020-05-23 01:05:51 -04:00
marketplacePlugins , err := marketplaceClient . GetPlugins ( filter )
2019-09-17 15:02:26 -04:00
if err != nil {
2022-08-18 05:01:37 -04:00
return nil , model . NewAppError ( "getRemotePlugins" , "app.plugin.marketplace_client.failed_to_fetch" , nil , "" , http . StatusInternalServerError ) . Wrap ( err )
2019-09-17 15:02:26 -04:00
}
for _ , p := range marketplacePlugins {
2020-01-15 13:38:55 -05:00
if p . Manifest == nil {
2019-09-17 15:02:26 -04:00
continue
}
2020-01-15 13:38:55 -05:00
result [ p . Manifest . Id ] = & model . MarketplacePlugin { BaseMarketplacePlugin : p }
}
return result , nil
}
// mergePrepackagedPlugins merges pre-packaged plugins to remote marketplace plugins list.
func ( a * App ) mergePrepackagedPlugins ( remoteMarketplacePlugins map [ string ] * model . MarketplacePlugin ) * model . AppError {
pluginsEnvironment := a . GetPluginsEnvironment ( )
if pluginsEnvironment == nil {
return model . NewAppError ( "mergePrepackagedPlugins" , "app.plugin.config.app_error" , nil , "" , http . StatusInternalServerError )
}
for _ , prepackaged := range pluginsEnvironment . PrepackagedPlugins ( ) {
if prepackaged . Manifest == nil {
continue
}
prepackagedMarketplace := & model . MarketplacePlugin {
BaseMarketplacePlugin : & model . BaseMarketplacePlugin {
2020-01-28 13:33:21 -05:00
HomepageURL : prepackaged . Manifest . HomepageURL ,
IconData : prepackaged . IconData ,
ReleaseNotesURL : prepackaged . Manifest . ReleaseNotesURL ,
Manifest : prepackaged . Manifest ,
2020-01-15 13:38:55 -05:00
} ,
}
// If not available in marketplace, add the prepackaged
if remoteMarketplacePlugins [ prepackaged . Manifest . Id ] == nil {
remoteMarketplacePlugins [ prepackaged . Manifest . Id ] = prepackagedMarketplace
continue
}
2022-01-19 23:37:27 -05:00
// If available in the marketplace, only overwrite if newer.
2020-01-15 13:38:55 -05:00
prepackagedVersion , err := semver . Parse ( prepackaged . Manifest . Version )
if err != nil {
2022-08-18 05:01:37 -04:00
return model . NewAppError ( "mergePrepackagedPlugins" , "app.plugin.invalid_version.app_error" , nil , "" , http . StatusBadRequest ) . Wrap ( err )
2020-01-15 13:38:55 -05:00
}
marketplacePlugin := remoteMarketplacePlugins [ prepackaged . Manifest . Id ]
marketplaceVersion , err := semver . Parse ( marketplacePlugin . Manifest . Version )
if err != nil {
2022-08-18 05:01:37 -04:00
return model . NewAppError ( "mergePrepackagedPlugins" , "app.plugin.invalid_version.app_error" , nil , "" , http . StatusBadRequest ) . Wrap ( err )
2019-09-17 15:02:26 -04:00
}
2020-01-15 13:38:55 -05:00
if prepackagedVersion . GT ( marketplaceVersion ) {
remoteMarketplacePlugins [ prepackaged . Manifest . Id ] = prepackagedMarketplace
2019-09-17 15:02:26 -04:00
}
2020-01-15 13:38:55 -05:00
}
return nil
}
2019-09-17 15:02:26 -04:00
2020-01-15 13:38:55 -05:00
// mergeLocalPlugins merges locally installed plugins to remote marketplace plugins list.
2024-04-24 05:52:33 -04:00
func ( a * App ) mergeLocalPlugins ( rctx request . CTX , remoteMarketplacePlugins map [ string ] * model . MarketplacePlugin ) * model . AppError {
2020-01-15 13:38:55 -05:00
pluginsEnvironment := a . GetPluginsEnvironment ( )
if pluginsEnvironment == nil {
return model . NewAppError ( "GetMarketplacePlugins" , "app.plugin.config.app_error" , nil , "" , http . StatusInternalServerError )
2019-09-17 15:02:26 -04:00
}
2020-01-15 13:38:55 -05:00
localPlugins , err := pluginsEnvironment . Available ( )
2019-09-17 15:02:26 -04:00
if err != nil {
2022-08-18 05:01:37 -04:00
return model . NewAppError ( "GetMarketplacePlugins" , "app.plugin.config.app_error" , nil , "" , http . StatusInternalServerError ) . Wrap ( err )
2019-09-17 15:02:26 -04:00
}
2020-01-15 13:38:55 -05:00
for _ , plugin := range localPlugins {
if plugin . Manifest == nil {
continue
}
if remoteMarketplacePlugins [ plugin . Manifest . Id ] != nil {
// Remote plugin is installed.
remoteMarketplacePlugins [ plugin . Manifest . Id ] . InstalledVersion = plugin . Manifest . Version
2019-09-17 15:02:26 -04:00
continue
}
2020-01-20 10:34:18 -05:00
iconData := ""
if plugin . Manifest . IconPath != "" {
iconData , err = getIcon ( filepath . Join ( plugin . Path , plugin . Manifest . IconPath ) )
if err != nil {
2024-04-24 05:52:33 -04:00
rctx . Logger ( ) . Warn ( "Error loading local plugin icon" , mlog . String ( "plugin_id" , plugin . Manifest . Id ) , mlog . String ( "icon_path" , plugin . Manifest . IconPath ) , mlog . Err ( err ) )
2020-01-20 10:34:18 -05:00
}
}
2020-01-15 13:38:55 -05:00
var labels [ ] model . MarketplaceLabel
if * a . Config ( ) . PluginSettings . EnableRemoteMarketplace {
// Labels should not (yet) be localized as the labels sent by the Marketplace are not (yet) localizable.
labels = append ( labels , model . MarketplaceLabel {
Name : "Local" ,
Description : "This plugin is not listed in the marketplace" ,
} )
}
remoteMarketplacePlugins [ plugin . Manifest . Id ] = & model . MarketplacePlugin {
2019-09-17 15:02:26 -04:00
BaseMarketplacePlugin : & model . BaseMarketplacePlugin {
2020-01-28 13:33:21 -05:00
HomepageURL : plugin . Manifest . HomepageURL ,
IconData : iconData ,
ReleaseNotesURL : plugin . Manifest . ReleaseNotesURL ,
Labels : labels ,
Manifest : plugin . Manifest ,
2019-09-17 15:02:26 -04:00
} ,
InstalledVersion : plugin . Manifest . Version ,
2020-01-15 13:38:55 -05:00
}
2019-09-17 15:02:26 -04:00
}
2020-01-15 13:38:55 -05:00
return nil
2019-09-17 15:02:26 -04:00
}
2020-06-30 08:17:00 -04:00
func ( a * App ) getBaseMarketplaceFilter ( ) * model . MarketplacePluginFilter {
2022-12-21 14:10:26 -05:00
return a . ch . getBaseMarketplaceFilter ( )
2021-05-11 06:00:44 -04:00
}
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) getBaseMarketplaceFilter ( ) * model . MarketplacePluginFilter {
2020-06-30 08:17:00 -04:00
filter := & model . MarketplacePluginFilter {
ServerVersion : model . CurrentVersion ,
}
2022-12-21 14:10:26 -05:00
license := ch . srv . License ( )
2021-08-24 14:37:58 -04:00
if license != nil && license . HasEnterpriseMarketplacePlugins ( ) {
2020-06-30 08:17:00 -04:00
filter . EnterprisePlugins = true
}
2022-11-02 16:40:26 -04:00
if license != nil && license . IsCloud ( ) {
2020-10-26 16:57:19 -04:00
filter . Cloud = true
}
2020-06-30 08:17:00 -04:00
if model . BuildEnterpriseReady == "true" {
filter . BuildEnterpriseReady = true
}
2020-10-05 13:01:33 -04:00
filter . Platform = runtime . GOOS + "-" + runtime . GOARCH
2020-06-30 08:17:00 -04:00
return filter
}
2019-09-17 15:02:26 -04:00
func pluginMatchesFilter ( manifest * model . Manifest , filter string ) bool {
filter = strings . TrimSpace ( strings . ToLower ( filter ) )
if filter == "" {
return true
}
if strings . ToLower ( manifest . Id ) == filter {
return true
}
if strings . Contains ( strings . ToLower ( manifest . Name ) , filter ) {
return true
}
if strings . Contains ( strings . ToLower ( manifest . Description ) , filter ) {
return true
}
return false
}
2019-08-22 15:17:47 -04:00
// notifyPluginEnabled notifies connected websocket clients across all peers if the version of the given
// plugin is same across them.
//
// When a peer finds itself in agreement with all other peers as to the version of the given plugin,
// it will notify all connected websocket clients (across all peers) to trigger the (re-)installation.
// There is a small chance that this never occurs, because the last server to finish installing dies before it can announce.
// There is also a chance that multiple servers notify, but the webapp handles this idempotently.
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) notifyPluginEnabled ( manifest * model . Manifest ) error {
pluginsEnvironment := ch . GetPluginsEnvironment ( )
2019-08-22 15:17:47 -04:00
if pluginsEnvironment == nil {
return errors . New ( "pluginsEnvironment is nil" )
}
if ! manifest . HasClient ( ) || ! pluginsEnvironment . IsActive ( manifest . Id ) {
return nil
}
var statuses model . PluginStatuses
2022-12-21 14:10:26 -05:00
if ch . srv . platform . Cluster ( ) != nil {
2019-08-22 15:17:47 -04:00
var err * model . AppError
2022-12-21 14:10:26 -05:00
statuses , err = ch . srv . platform . Cluster ( ) . GetPluginStatuses ( )
2019-08-22 15:17:47 -04:00
if err != nil {
return err
}
}
2022-12-21 14:10:26 -05:00
localStatus , err := ch . GetPluginStatus ( manifest . Id )
2019-08-22 15:17:47 -04:00
if err != nil {
return err
}
statuses = append ( statuses , localStatus )
// This will not guard against the race condition of enabling a plugin immediately after installation.
// As GetPluginStatuses() will not return the new plugin (since other peers are racing to install),
// this peer will end up checking status against itself and will notify all webclients (including peer webclients),
// which may result in a 404.
for _ , status := range statuses {
if status . PluginId == manifest . Id && status . Version != manifest . Version {
2023-08-08 17:29:57 -04:00
ch . srv . Log ( ) . Debug ( "Not ready to notify webclients" , mlog . String ( "cluster_id" , status . ClusterId ) , mlog . String ( "plugin_id" , manifest . Id ) )
2019-08-22 15:17:47 -04:00
return nil
}
}
// Notify all cluster peer clients.
2022-09-02 06:17:22 -04:00
message := model . NewWebSocketEvent ( model . WebsocketEventPluginEnabled , "" , "" , "" , nil , "" )
2019-08-22 15:17:47 -04:00
message . Add ( "manifest" , manifest . ClientManifest ( ) )
2022-12-21 14:10:26 -05:00
ch . srv . platform . Publish ( message )
2019-08-22 15:17:47 -04:00
return nil
}
2019-11-18 19:02:41 -05:00
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) getPluginsFromFolder ( ) ( map [ string ] * pluginSignaturePath , * model . AppError ) {
fileStorePaths , appErr := ch . srv . listDirectory ( fileStorePluginFolder , false )
2019-11-18 19:02:41 -05:00
if appErr != nil {
2022-08-18 05:01:37 -04:00
return nil , model . NewAppError ( "getPluginsFromDir" , "app.plugin.sync.list_filestore.app_error" , nil , "" , http . StatusInternalServerError ) . Wrap ( appErr )
2019-11-18 19:02:41 -05:00
}
2019-12-12 12:45:55 -05:00
2022-12-21 14:10:26 -05:00
return ch . getPluginsFromFilePaths ( fileStorePaths ) , nil
2020-01-15 13:38:55 -05:00
}
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) getPluginsFromFilePaths ( fileStorePaths [ ] string ) map [ string ] * pluginSignaturePath {
2019-11-18 19:02:41 -05:00
pluginSignaturePathMap := make ( map [ string ] * pluginSignaturePath )
for _ , path := range fileStorePaths {
if strings . HasSuffix ( path , ".tar.gz" ) {
id := strings . TrimSuffix ( filepath . Base ( path ) , ".tar.gz" )
helper := & pluginSignaturePath {
2021-02-25 14:22:27 -05:00
pluginID : id ,
2023-08-08 17:29:57 -04:00
bundlePath : path ,
2019-11-18 19:02:41 -05:00
signaturePath : "" ,
}
pluginSignaturePathMap [ id ] = helper
}
}
for _ , path := range fileStorePaths {
2019-12-12 12:45:55 -05:00
if strings . HasSuffix ( path , ".tar.gz.sig" ) {
id := strings . TrimSuffix ( filepath . Base ( path ) , ".tar.gz.sig" )
2019-11-18 19:02:41 -05:00
if val , ok := pluginSignaturePathMap [ id ] ; ! ok {
2023-08-09 10:54:41 -04:00
ch . srv . Log ( ) . Warn ( "Unknown signature" , mlog . String ( "path" , path ) )
2019-11-18 19:02:41 -05:00
} else {
val . signaturePath = path
}
}
}
2019-12-12 12:45:55 -05:00
2020-01-15 13:38:55 -05:00
return pluginSignaturePathMap
}
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
// processPrepackagedPlugins processes the plugins prepackaged with this server in the
// prepackaged_plugins directory.
//
// If enabled, prepackaged plugins are installed or upgraded locally. A list of transitionally
// prepackaged plugins is also collected for later persistence to the filestore.
func ( ch * Channels ) processPrepackagedPlugins ( prepackagedPluginsDir string ) error {
2025-06-24 14:11:02 -04:00
logger := ch . srv . Log ( )
logger . Info ( "Processing prepackaged plugin" )
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
prepackagedPluginsPath , found := fileutils . FindDir ( prepackagedPluginsDir )
2020-01-15 13:38:55 -05:00
if ! found {
2025-06-24 14:11:02 -04:00
logger . Debug ( "No prepackaged plugins directory found" )
2020-01-15 13:38:55 -05:00
return nil
}
2025-06-24 14:11:02 -04:00
logger = logger . With (
mlog . String ( "prepackaged_plugins_path" , prepackagedPluginsPath ) ,
)
ch . srv . Log ( ) . Debug ( "Processing prepackaged plugins in directory" )
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
2020-02-17 17:30:48 -05:00
var fileStorePaths [ ] string
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
err := filepath . Walk ( prepackagedPluginsPath , func ( walkPath string , info os . FileInfo , err error ) error {
2020-01-15 13:38:55 -05:00
fileStorePaths = append ( fileStorePaths , walkPath )
return nil
} )
if err != nil {
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
return errors . Wrap ( err , "failed to walk prepackaged plugins" )
2020-01-15 13:38:55 -05:00
}
2022-12-21 14:10:26 -05:00
pluginSignaturePathMap := ch . getPluginsFromFilePaths ( fileStorePaths )
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
plugins := make ( chan * plugin . PrepackagedPlugin , len ( pluginSignaturePathMap ) )
// Before processing any prepackaged plugins, take a snapshot of the available manifests
// to decide what was synced from the filestore.
pluginsEnvironment := ch . GetPluginsEnvironment ( )
if pluginsEnvironment == nil {
return errors . New ( "pluginsEnvironment is nil" )
}
availablePlugins , err := pluginsEnvironment . Available ( )
if err != nil {
return errors . Wrap ( err , "failed to list available plugins" )
}
availablePluginsMap := make ( map [ string ] * model . BundleInfo , len ( availablePlugins ) )
for _ , bundleInfo := range availablePlugins {
availablePluginsMap [ bundleInfo . Manifest . Id ] = bundleInfo
}
2020-02-17 17:30:48 -05:00
var wg sync . WaitGroup
for _ , psPath := range pluginSignaturePathMap {
wg . Add ( 1 )
go func ( psPath * pluginSignaturePath ) {
defer wg . Done ( )
2022-12-21 14:10:26 -05:00
p , err := ch . processPrepackagedPlugin ( psPath )
2020-02-17 17:30:48 -05:00
if err != nil {
2023-04-14 04:02:05 -04:00
var appErr * model . AppError
2023-08-08 17:29:57 -04:00
if errors . As ( err , & appErr ) && appErr . Id == "app.plugin.skip_installation.app_error" {
2023-04-14 04:02:05 -04:00
return
}
2025-06-24 14:11:02 -04:00
logger . Error ( "Failed to install prepackaged plugin" , mlog . String ( "bundle_path" , psPath . bundlePath ) , mlog . Err ( err ) )
2020-02-17 17:30:48 -05:00
return
}
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
plugins <- p
2020-02-17 17:30:48 -05:00
} ( psPath )
}
wg . Wait ( )
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
close ( plugins )
prepackagedPlugins := make ( [ ] * plugin . PrepackagedPlugin , 0 , len ( pluginSignaturePathMap ) )
transitionallyPrepackagedPlugins := make ( [ ] * plugin . PrepackagedPlugin , 0 )
for p := range plugins {
2024-08-14 13:22:18 -04:00
if ch . pluginIsTransitionallyPrepackaged ( p . Manifest ) {
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
if ch . shouldPersistTransitionallyPrepackagedPlugin ( availablePluginsMap , p ) {
transitionallyPrepackagedPlugins = append ( transitionallyPrepackagedPlugins , p )
}
} else {
prepackagedPlugins = append ( prepackagedPlugins , p )
}
2020-01-15 13:38:55 -05:00
}
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
pluginsEnvironment . SetPrepackagedPlugins ( prepackagedPlugins , transitionallyPrepackagedPlugins )
return nil
2020-01-15 13:38:55 -05:00
}
// processPrepackagedPlugin will return the prepackaged plugin metadata and will also
// install the prepackaged plugin if it had been previously enabled and AutomaticPrepackagedPlugins is true.
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) processPrepackagedPlugin ( pluginPath * pluginSignaturePath ) ( * plugin . PrepackagedPlugin , error ) {
2025-06-24 14:11:02 -04:00
logger := ch . srv . Log ( ) . With (
mlog . String ( "bundle_path" , pluginPath . bundlePath ) ,
mlog . String ( "signature_path" , pluginPath . signaturePath ) ,
)
2020-01-15 13:38:55 -05:00
2023-08-08 17:29:57 -04:00
logger . Info ( "Processing prepackaged plugin" )
fileReader , err := os . Open ( pluginPath . bundlePath )
2020-01-15 13:38:55 -05:00
if err != nil {
2023-08-08 17:29:57 -04:00
return nil , errors . Wrapf ( err , "Failed to open prepackaged plugin %s" , pluginPath . bundlePath )
2020-01-15 13:38:55 -05:00
}
2020-02-17 17:30:48 -05:00
defer fileReader . Close ( )
2022-08-09 07:25:46 -04:00
tmpDir , err := os . MkdirTemp ( "" , "plugintmp" )
2020-01-15 13:38:55 -05:00
if err != nil {
return nil , errors . Wrap ( err , "Failed to create temp dir plugintmp" )
}
defer os . RemoveAll ( tmpDir )
2025-06-24 14:11:02 -04:00
plugin , pluginDir , err := ch . buildPrepackagedPlugin ( logger , pluginPath , fileReader , tmpDir )
2020-01-15 13:38:55 -05:00
if err != nil {
2023-08-08 17:29:57 -04:00
return nil , errors . Wrapf ( err , "Failed to get prepackaged plugin %s" , pluginPath . bundlePath )
2020-01-15 13:38:55 -05:00
}
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
logger = logger . With ( mlog . String ( "plugin_id" , plugin . Manifest . Id ) )
2020-01-15 13:38:55 -05:00
// Skip installing the plugin at all if automatic prepackaged plugins is disabled
2022-12-21 14:10:26 -05:00
if ! * ch . cfgSvc . Config ( ) . PluginSettings . AutomaticPrepackagedPlugins {
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
logger . Info ( "Not installing prepackaged plugin: automatic prepackaged plugins disabled" )
2020-01-15 13:38:55 -05:00
return plugin , nil
}
// Skip installing if the plugin is has not been previously enabled.
2022-12-21 14:10:26 -05:00
pluginState := ch . cfgSvc . Config ( ) . PluginSettings . PluginStates [ plugin . Manifest . Id ]
2020-01-15 13:38:55 -05:00
if pluginState == nil || ! pluginState . Enable {
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
logger . Info ( "Not installing prepackaged plugin: not previously enabled" )
2020-01-15 13:38:55 -05:00
return plugin , nil
}
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
if _ , err := ch . installExtractedPlugin ( plugin . Manifest , pluginDir , installPluginLocallyOnlyIfNewOrUpgrade ) ; err != nil && err . Id != "app.plugin.skip_installation.app_error" {
2023-08-08 17:29:57 -04:00
return nil , errors . Wrapf ( err , "Failed to install extracted prepackaged plugin %s" , pluginPath . bundlePath )
2020-01-15 13:38:55 -05:00
}
return plugin , nil
}
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
var transitionallyPrepackagedPlugins = [ ] string {
"antivirus" ,
"focalboard" ,
"mattermost-autolink" ,
"com.mattermost.aws-sns" ,
"com.mattermost.confluence" ,
"com.mattermost.custom-attributes" ,
"jenkins" ,
"jitsi" ,
"com.mattermost.plugin-todo" ,
"com.mattermost.welcomebot" ,
"com.mattermost.apps" ,
}
// pluginIsTransitionallyPrepackaged identifies plugin ids that are currently prepackaged but
// slated for future removal.
2024-08-14 13:22:18 -04:00
func ( ch * Channels ) pluginIsTransitionallyPrepackaged ( m * model . Manifest ) bool {
2025-08-27 16:07:31 -04:00
return slices . Contains ( transitionallyPrepackagedPlugins , m . Id )
2024-08-14 13:22:18 -04:00
}
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
// shouldPersistTransitionallyPrepackagedPlugin determines if a transitionally prepackaged plugin
// should be persisted to the filestore, taking into account whether it's already enabled and
// would improve on what's already in the filestore.
func ( ch * Channels ) shouldPersistTransitionallyPrepackagedPlugin ( availablePluginsMap map [ string ] * model . BundleInfo , p * plugin . PrepackagedPlugin ) bool {
logger := ch . srv . Log ( ) . With ( mlog . String ( "plugin_id" , p . Manifest . Id ) , mlog . String ( "prepackaged_version" , p . Manifest . Version ) )
// Ignore the plugin altogether unless it was previously enabled.
pluginState := ch . cfgSvc . Config ( ) . PluginSettings . PluginStates [ p . Manifest . Id ]
if pluginState == nil || ! pluginState . Enable {
logger . Debug ( "Should not persist transitionally prepackaged plugin: not previously enabled" )
return false
}
// Ignore the plugin if the same or newer version is already available
// (having previously synced from the filestore).
existing , found := availablePluginsMap [ p . Manifest . Id ]
if ! found {
logger . Info ( "Should persist transitionally prepackaged plugin: not currently in filestore" )
return true
}
prepackagedVersion , err := semver . Parse ( p . Manifest . Version )
if err != nil {
logger . Error ( "Should not persist transitionally prepackged plugin: invalid prepackaged version" , mlog . Err ( err ) )
return false
}
logger = logger . With ( mlog . String ( "existing_version" , existing . Manifest . Version ) )
existingVersion , err := semver . Parse ( existing . Manifest . Version )
if err != nil {
// Consider this an old version and replace with the prepackaged version instead.
logger . Warn ( "Should persist transitionally prepackged plugin: invalid existing version" , mlog . Err ( err ) )
return true
}
if prepackagedVersion . GT ( existingVersion ) {
logger . Info ( "Should persist transitionally prepackged plugin: newer version" )
return true
}
logger . Info ( "Should not persist transitionally prepackged plugin: not a newer version" )
return false
}
// persistTransitionallyPrepackagedPlugins writes plugins that are transitionally prepackaged with
// the server to the filestore to allow their continued use when the plugin eventually stops being
// prepackaged.
//
// We identify which plugins need to be persisted during startup via processPrepackagedPlugins.
// Once we persist the set of plugins to the filestore, we clear the list to prevent this server
// from trying again.
//
// In a multi-server cluster, only the cluster leader should persist these plugins to avoid
// concurrent writes to the filestore. But during an upgrade, there's no guarantee that a freshly
// upgraded server will be the cluster leader to perform this step in a timely fashion, so the
// persistence has to be able to happen sometime after startup. Additionally, while this is a
// kind of migration, it's not a one off: new versions of these plugins may still be shipped
// during the transition period, or new plugins may be added to the list.
//
// So instead of a one-time migration, we opt to run this method every time the cluster leader
// changes, but minimizing rework. More than one server may end up persisting the same plugin
// (but never concurrently!), but all servers will eventually converge on this method becoming a
// no-op (until this set of plugins changes in a subsequent release).
//
// Finally, if an error occurs persisting the plugin, we don't try again until the server restarts,
// or another server becomes cluster leader.
func ( ch * Channels ) persistTransitionallyPrepackagedPlugins ( ) {
if ! ch . srv . IsLeader ( ) {
ch . srv . Log ( ) . Debug ( "Not persisting transitionally prepackaged plugins: not the leader" )
return
}
pluginsEnvironment := ch . GetPluginsEnvironment ( )
if pluginsEnvironment == nil {
ch . srv . Log ( ) . Debug ( "Not persisting transitionally prepackaged plugins: no plugin environment" )
return
}
transitionallyPrepackagedPlugins := pluginsEnvironment . TransitionallyPrepackagedPlugins ( )
if len ( transitionallyPrepackagedPlugins ) == 0 {
ch . srv . Log ( ) . Debug ( "Not persisting transitionally prepackaged plugins: none found" )
return
}
var wg sync . WaitGroup
for _ , p := range transitionallyPrepackagedPlugins {
wg . Add ( 1 )
go func ( p * plugin . PrepackagedPlugin ) {
defer wg . Done ( )
2025-06-24 14:11:02 -04:00
logger := ch . srv . Log ( ) . With (
mlog . String ( "plugin_id" , p . Manifest . Id ) ,
mlog . String ( "version" , p . Manifest . Version ) ,
mlog . String ( "bundle_path" , p . Path ) ,
mlog . String ( "signature_path" , p . SignaturePath ) ,
)
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
logger . Info ( "Persisting transitionally prepackaged plugin" )
bundleReader , err := os . Open ( p . Path )
if err != nil {
logger . Error ( "Failed to read transitionally prepackaged plugin" , mlog . Err ( err ) )
2025-06-24 14:11:02 -04:00
return
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
}
defer bundleReader . Close ( )
2025-06-24 14:11:02 -04:00
signatureReader , err := os . Open ( p . SignaturePath )
if err != nil {
logger . Error ( "Failed to read transitionally prepackaged plugin signature" , mlog . Err ( err ) )
return
}
defer signatureReader . Close ( )
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
// Write the plugin to the filestore, but don't bother notifying the peers,
// as there's no reason to reload the plugin to run the same version again.
appErr := ch . installPluginToFilestore ( p . Manifest , bundleReader , signatureReader )
if appErr != nil {
2025-06-24 14:11:02 -04:00
logger . Error ( "Failed to persist transitionally prepackaged plugin" , mlog . Err ( appErr ) )
MM-53355: install transitionally prepackaged plugins to filestore (#24225)
* move plugin signature verification to caller
The semantics for when plugin signature validation is required are unique to the caller, so move this logic there instead of masking it, thus simplifying some of the downstream code.
* support transitionally prepacked plugins
Transitionally prepackaged plugins are prepackaged plugins slated for unpackaging in some future release. Like prepackaged plugins, they automatically install or upgrade if the server is configured to enable that plugin, but unlike prepackaged plugins they don't add to the marketplace to allow for offline installs. In fact, if unlisted from the marketplace and not already enabled via `config.json`, a transitionally prepackaged plugin is essentially hidden.
To ensure a smooth transition in the future release when this plugin is no longer prepackaged at all, transitionally prepackaged plugins are persisted to the filestore as if they had been installed by the enduser. On the next restart, even while the plugin is still transitionally prepackaged, the version in the filestore will take priority. It remains possible for a transitionally prepackaged plugin to upgrade (and once again persist) if we ship a newer version before dropping it altogether.
Some complexity arises in a multi-server cluster, primarily because we don't want to deal with multiple servers writing the same object to the filestore. This is probably fine for S3, but has undefined semantics for regular filesystems, especially with some customers backing their files on any number of different fileshare technologies. To simplify the complexity, only the cluster leader persists transitionally prepackaged plugins.
Unfortunately, this too is complicated, since on upgrade to the first version with the transitionally prepackaged plugin, there is no guarantee that server will be the leader. In fact, as all nodes restart, there is no guarantee that any newly started server will start as the leader. So the persistence has to happen in a job-like fashion. The migration system might work, except we want the ability to run this repeatedly as we add to (or update) these transitionally prepackaged plugins. We also want to minimize the overhead required from the server to juggle any of this.
As a consequence, the persistence of transitionally prepackaged plugins occurs on every cluster leader change. Each server will try at most once to persist its collection of transitionally prepackaged plugins, and newly started servers will see the plugins in the filestore and skip this step altogether.
The current set of transitionally prepackaged plugins include the following, but this is expected to change:
* focalboard
* complete list of transitionally prepackaged plugins
* update plugin_install.go docs
* updated test plugins
* unit test transitionally prepackged plugins
* try restoring original working directory
* Apply suggestions from code review
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
* clarify processPrepackagedPlugins comment
---------
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2023-08-17 11:46:57 -04:00
}
} ( p )
}
wg . Wait ( )
pluginsEnvironment . ClearTransitionallyPrepackagedPlugins ( )
ch . srv . Log ( ) . Info ( "Finished persisting transitionally prepackaged plugins" )
}
2023-08-08 17:29:57 -04:00
// buildPrepackagedPlugin builds a PrepackagedPlugin from the plugin at the given path, additionally returning the directory in which it was extracted.
2025-06-24 14:11:02 -04:00
func ( ch * Channels ) buildPrepackagedPlugin ( logger * mlog . Logger , pluginPath * pluginSignaturePath , pluginFile io . ReadSeeker , tmpDir string ) ( * plugin . PrepackagedPlugin , string , error ) {
// Always require signature for prepackaged plugins
if pluginPath . signaturePath == "" {
return nil , "" , errors . Errorf ( "Prepackaged plugin missing required signature file" )
}
// Open signature file
signatureFile , sigErr := os . Open ( pluginPath . signaturePath )
if sigErr != nil {
return nil , "" , errors . Wrapf ( sigErr , "Failed to open prepackaged plugin signature %s" , pluginPath . signaturePath )
}
defer signatureFile . Close ( )
// Verify signature extraction
if _ , err := pluginFile . Seek ( 0 , io . SeekStart ) ; err != nil {
return nil , "" , errors . Wrapf ( err , "Failed to seek to start of plugin file for signature verification: %s" , pluginPath . bundlePath )
}
if appErr := ch . verifyPlugin ( logger , pluginFile , signatureFile ) ; appErr != nil {
return nil , "" , errors . Wrapf ( appErr , "Prepackaged plugin signature verification failed for %s using %s" , pluginPath . bundlePath , pluginPath . signaturePath )
}
// Extract plugin after signature verification
if _ , err := pluginFile . Seek ( 0 , io . SeekStart ) ; err != nil {
return nil , "" , errors . Wrapf ( err , "Failed to seek to start of plugin file for extraction: %s" , pluginPath . bundlePath )
}
2020-01-15 13:38:55 -05:00
manifest , pluginDir , appErr := extractPlugin ( pluginFile , tmpDir )
if appErr != nil {
2023-08-08 17:29:57 -04:00
return nil , "" , errors . Wrapf ( appErr , "Failed to extract plugin with path %s" , pluginPath . bundlePath )
2020-01-15 13:38:55 -05:00
}
plugin := new ( plugin . PrepackagedPlugin )
plugin . Manifest = manifest
2023-08-08 17:29:57 -04:00
plugin . Path = pluginPath . bundlePath
2025-06-24 14:11:02 -04:00
plugin . SignaturePath = pluginPath . signaturePath
2020-01-15 13:38:55 -05:00
if manifest . IconPath != "" {
2020-01-20 10:34:18 -05:00
iconData , err := getIcon ( filepath . Join ( pluginDir , manifest . IconPath ) )
2020-01-15 13:38:55 -05:00
if err != nil {
2025-06-24 14:11:02 -04:00
logger . Warn ( "Error loading local plugin icon" , mlog . String ( "icon_path" , plugin . Manifest . IconPath ) , mlog . Err ( err ) )
2020-01-15 13:38:55 -05:00
}
plugin . IconData = iconData
}
return plugin , pluginDir , nil
}
func getIcon ( iconPath string ) ( string , error ) {
2022-08-09 07:25:46 -04:00
icon , err := os . ReadFile ( iconPath )
2020-01-15 13:38:55 -05:00
if err != nil {
return "" , errors . Wrapf ( err , "failed to open icon at path %s" , iconPath )
}
2020-06-13 03:51:45 -04:00
2020-01-15 13:38:55 -05:00
if ! svg . Is ( icon ) {
2020-06-13 03:51:45 -04:00
return "" , errors . Errorf ( "icon is not svg %s" , iconPath )
2020-01-15 13:38:55 -05:00
}
2020-06-13 03:51:45 -04:00
2020-01-15 13:38:55 -05:00
return fmt . Sprintf ( "data:image/svg+xml;base64,%s" , base64 . StdEncoding . EncodeToString ( icon ) ) , nil
2019-11-18 19:02:41 -05:00
}
2022-05-10 13:29:48 -04:00
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) getPluginStateOverride ( pluginID string ) ( bool , bool ) {
2022-05-10 13:29:48 -04:00
switch pluginID {
2022-05-25 12:36:08 -04:00
case model . PluginIdApps :
2022-05-10 13:29:48 -04:00
// Tie Apps proxy disabled status to the feature flag.
2022-12-21 14:10:26 -05:00
if ! ch . cfgSvc . Config ( ) . FeatureFlags . AppsEnabled {
2022-05-10 13:29:48 -04:00
return true , false
}
}
return false , false
}
2023-01-25 12:24:01 -05:00
func ( a * App ) IsPluginActive ( pluginName string ) ( bool , error ) {
return a . Channels ( ) . IsPluginActive ( pluginName )
}
func ( ch * Channels ) IsPluginActive ( pluginName string ) ( bool , error ) {
pluginStatus , err := ch . GetPluginStatus ( pluginName )
if err != nil {
return false , err
}
return pluginStatus . State == model . PluginStateRunning , nil
}