mattermost/server/public/plugin/environment.go

713 lines
21 KiB
Go
Raw Permalink Normal View History

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"fmt"
"hash/fnv"
"os"
"path/filepath"
"sync"
"time"
plugin "github.com/hashicorp/go-plugin"
"github.com/pkg/errors"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/utils"
)
MM-17023: Plugin Marketplace (#12183) * MM-17149 - Extend config.json for marketplace settings (#11933) * MM-17149 - Extend config.json for marketplace settings * Renamed MarketplaceUrl, tracking default marketplace url * Added EnableMarketplace to the client config * Revert "Added EnableMarketplace to the client config" This reverts commit 0f982c4c661c2cd9bb96264e9a01a2363c40d9c5. * MM-17149 - Added EnableMarketplace to the client config (#11958) * Added EnableMarketplace to the client config * Moved EnableMarketplace setting out of limited client configuration * MM-17150, MM-17545, MM-18100 - Implement GET /api/v4/plugins/m… (#11977) * MM-17150 - Implement GET /api/v4/plugins/marketplace proxying upstream MM-17545 - Merge locally installed plugins into GET /api/v4/plugins/marketplace * Replaced MarketplacePluginState with Installed * Setting InstalledVersion instead of Installed * marketplace client setting per_page if non zero * Creating insecure client for marketplace url * Fixed trailing slash for default marketplace url * Adding filtering * Fixed function names * Renamed Manifest() to GetManifest(), added godoc for BaseMarketplacePlugin * Handling plugin.ErrNotFound correctly * Checking err == nil instead when a plugin is installed * MM-18450 - Local-only plugin search (#12152) * MM-17846: plugin icons (#12157) * MM-17846: add support for plugin icons Extend the model definitions to support plugin icons from the marketplace. * s/IconURL/IconData * MM-18475 - Converge on snake_case responses from the marketplace (#12179) * MM-18520 - MM-Server should forward server version to marketplace server (#12181) * Renamed request to filter client4.GetMarketplacePlugins * Renamed request to filter * Guarding against bad marketplace server response
2019-09-17 15:02:26 -04:00
var ErrNotFound = errors.New("Item not found")
type apiImplCreatorFunc func(*model.Manifest) API
// registeredPlugin stores the state for a given plugin that has been activated
// or attempted to be activated this server run.
//
// If an installed plugin is missing from the env.registeredPlugins map, then the
// plugin is configured as disabled and has not been activated during this server run.
type registeredPlugin struct {
BundleInfo *model.BundleInfo
State int
Error string
supervisor *supervisor
}
MM-19606- Rework Prepackaged Plugins (#13449) * MM-19609 - Add new prepackage configuration settings (#13062) * Add signatures to the prepackaged plugins (#13138) * MM-19612 - Support querying local plugin marketplace when upst… (#13250) * MM-19612 - Support querying local plugin marketplace when upstream unavailable or disabled * Update translations file * Fixed comment * Updated to check EnableRemoteMarketplace setting and LocalOnly to get marketplace plugins * Fixed unit tests * Tests cleanup code * Removed unused error message * Updated tests * MM-19614- Updated Marketplace Service error id (#13388) * [MM-19610] Consume prepackaged plugins (#13005) * consume prepackaged plugins into memory * missing i18n * remove spurious .gitignore changes * return on failure to install prepackged plugins * cleanup * s/plugins/availablePlugins * whitespace * don't return extractDir when not needed * s/plug/plugin * error on icon, cleanup * update armored version of testplugin signature * honour AutomaticPrepackagedPlugins * document getPrepackagedPlugin * MM-19613 - Include prepackaged plugins in marketplace results (#13433) * Added prepackaged plugins to marketplace results * PR Feedback * PR Feedback * Update error where definition * Removing unnecessary var declaration * Updated comments * MM-21263 - Use EnableRemoteMarketplace in marketplace install… (#13438) * MM-21263 - Use EnableRemoteMarketplace in marketplace install endpoint * Call updateConfig before calling NewServer in TestHelper * Added translations * PR feedback * Translations * Feedback * s/helpers.go/download.go * Converging env.PrepackagedPlugins * Initial PR feedback * Ordered imports properly * Updated DownloadURL to return slice of bytes * Fixed method typo * Fixed logging * Added read lock for prepackaged plugins list * PR Feedback * Added condition to only install prepackaged plugin if it was previously enabled * Linting * Updated to check plugin state in config * Closing filereader * Only add local label if remote marketplace is enabled * Updated local tag description * Fixed tests Co-authored-by: Ali Farooq <ali.farooq0@pm.me> Co-authored-by: Shota Gvinepadze <wineson@gmail.com> Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
2020-01-15 13:38:55 -05:00
// PrepackagedPlugin is a plugin prepackaged with the server and found on startup.
type PrepackagedPlugin struct {
Always require signatures for prepackaged plugins (#31785) * Always require signatures for prepackaged plugins We have always required signatures for packages installed via the marketplace -- whether remotely satisfied, or sourced from the prepackaged plugin cache. However, prepackaged plugins discovered and automatically installed on startup did not require a valid signature. Since we already ship signatures for all Mattermost-authored prepackaged plugins, it's easy to simply start requiring this. Distributions of Mattermost that bundle their own prepackaged plugins will have to include their own signatures. This in turn requires distributing and configuring Mattermost with a custom public key via `PluginSettings.SignaturePublicKeyFiles`. Note that this enhanced security is neutered with a deployment that uses a file-based `config.json`, as any exploit that allows appending to the prepackaged plugins cache probably also allows modifying `config.json` to register a new public key. A [database-based config](https://docs.mattermost.com/configure/configuration-in-your-database.html) is recommended. Finally, we already support an optional setting `PluginSettings.RequirePluginSignature` to always require a plugin signature, although this effectively disables plugin uploads and requires extra effort to deploy the corresponding signature. In environments where only prepackaged plugins are used, this setting is ideal. Fixes: https://mattermost.atlassian.net/browse/MM-64627 * setup dev key, expect no plugins if sig fails * Fix shadow variable errors in test helpers Pre-declare signaturePublicKey variable in loops to avoid shadowing the outer err variable used in error handling. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Replace PrepackagedPlugin.Signature with SignaturePath for memory efficiency - Changed PrepackagedPlugin struct to use SignaturePath string instead of Signature []byte - Updated buildPrepackagedPlugin to use file descriptor instead of reading signature into memory - Modified plugin installation and persistence to read from signature file paths - Updated all tests to check SignaturePath instead of Signature field - Removed unused bytes import from plugin.go This change reduces memory usage by storing file paths instead of signature data in memory while maintaining the same security verification functionality. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
2025-06-24 14:11:02 -04:00
Path string
IconData string
Manifest *model.Manifest
SignaturePath string
MM-19606- Rework Prepackaged Plugins (#13449) * MM-19609 - Add new prepackage configuration settings (#13062) * Add signatures to the prepackaged plugins (#13138) * MM-19612 - Support querying local plugin marketplace when upst… (#13250) * MM-19612 - Support querying local plugin marketplace when upstream unavailable or disabled * Update translations file * Fixed comment * Updated to check EnableRemoteMarketplace setting and LocalOnly to get marketplace plugins * Fixed unit tests * Tests cleanup code * Removed unused error message * Updated tests * MM-19614- Updated Marketplace Service error id (#13388) * [MM-19610] Consume prepackaged plugins (#13005) * consume prepackaged plugins into memory * missing i18n * remove spurious .gitignore changes * return on failure to install prepackged plugins * cleanup * s/plugins/availablePlugins * whitespace * don't return extractDir when not needed * s/plug/plugin * error on icon, cleanup * update armored version of testplugin signature * honour AutomaticPrepackagedPlugins * document getPrepackagedPlugin * MM-19613 - Include prepackaged plugins in marketplace results (#13433) * Added prepackaged plugins to marketplace results * PR Feedback * PR Feedback * Update error where definition * Removing unnecessary var declaration * Updated comments * MM-21263 - Use EnableRemoteMarketplace in marketplace install… (#13438) * MM-21263 - Use EnableRemoteMarketplace in marketplace install endpoint * Call updateConfig before calling NewServer in TestHelper * Added translations * PR feedback * Translations * Feedback * s/helpers.go/download.go * Converging env.PrepackagedPlugins * Initial PR feedback * Ordered imports properly * Updated DownloadURL to return slice of bytes * Fixed method typo * Fixed logging * Added read lock for prepackaged plugins list * PR Feedback * Added condition to only install prepackaged plugin if it was previously enabled * Linting * Updated to check plugin state in config * Closing filereader * Only add local label if remote marketplace is enabled * Updated local tag description * Fixed tests Co-authored-by: Ali Farooq <ali.farooq0@pm.me> Co-authored-by: Shota Gvinepadze <wineson@gmail.com> Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
2020-01-15 13:38:55 -05:00
}
// Environment represents the execution environment of active plugins.
//
// It is meant for use by the Mattermost server to manipulate, interact with and report on the set
// of active plugins.
type Environment struct {
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
registeredPlugins sync.Map
pluginHealthCheckJob *PluginHealthCheckJob
logger *mlog.Logger
metrics metricsInterface
newAPIImpl apiImplCreatorFunc
dbDriver AppDriver
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
pluginDir string
webappPluginDir string
prepackagedPlugins []*PrepackagedPlugin
transitionallyPrepackagedPlugins []*PrepackagedPlugin
prepackagedPluginsLock sync.RWMutex
apiServerRegistrar APIServerRegistrar // For Python plugins to call back to Go API
}
func NewEnvironment(
newAPIImpl apiImplCreatorFunc,
dbDriver AppDriver,
pluginDir string,
webappPluginDir string,
DB driver implementation via RPC (#17779) This PR builds up on the pass-through DB driver to a fully functioning DB driver implementation via our RPC layer. To keep things separate from the plugin RPC API, and have the ability to move fast with changes, a separate field Driver is added to MattermostPlugin. Typically the field which is required to be compatible are the API and Helpers. It would be well-documented that Driver is purely for internal use by Mattermost plugins. A new Driver interface was created which would have a client and server implementation. Every object (connection, statement, etc.) is created and added to a map on the server side. On the client side, the wrapper structs hold the object id, and communicate via the RPC API using this id. When the server gets the object id, it picks up the appropriate object from its map and performs the operation, and sends back the data. Some things that need to be handled are errors. Typical error types like pq.Error and mysql.MySQLError are registered with encoding/gob. But for error variables like sql.ErrNoRows, a special integer is encoded with the ErrorString struct. And on the cilent side, the integer is checked, and the appropriate error variable is returned. Some pending things: - Context support. This is tricky. Since context.Context is an interface, it's not possible to marshal it. We have to find a way to get the timeout value from the context and pass it. - RowsColumnScanType(rowsID string, index int) reflect.Type API. Again, reflect.Type is an interface. - Master/Replica API support.
2021-06-16 23:23:52 -04:00
logger *mlog.Logger,
metrics metricsInterface,
) (*Environment, error) {
return &Environment{
logger: logger,
metrics: metrics,
newAPIImpl: newAPIImpl,
DB driver implementation via RPC (#17779) This PR builds up on the pass-through DB driver to a fully functioning DB driver implementation via our RPC layer. To keep things separate from the plugin RPC API, and have the ability to move fast with changes, a separate field Driver is added to MattermostPlugin. Typically the field which is required to be compatible are the API and Helpers. It would be well-documented that Driver is purely for internal use by Mattermost plugins. A new Driver interface was created which would have a client and server implementation. Every object (connection, statement, etc.) is created and added to a map on the server side. On the client side, the wrapper structs hold the object id, and communicate via the RPC API using this id. When the server gets the object id, it picks up the appropriate object from its map and performs the operation, and sends back the data. Some things that need to be handled are errors. Typical error types like pq.Error and mysql.MySQLError are registered with encoding/gob. But for error variables like sql.ErrNoRows, a special integer is encoded with the ErrorString struct. And on the cilent side, the integer is checked, and the appropriate error variable is returned. Some pending things: - Context support. This is tricky. Since context.Context is an interface, it's not possible to marshal it. We have to find a way to get the timeout value from the context and pass it. - RowsColumnScanType(rowsID string, index int) reflect.Type API. Again, reflect.Type is an interface. - Master/Replica API support.
2021-06-16 23:23:52 -04:00
dbDriver: dbDriver,
pluginDir: pluginDir,
webappPluginDir: webappPluginDir,
}, nil
}
// SetAPIServerRegistrar sets the function used to register the PluginAPI gRPC service
// for Python plugins. This must be called before activating Python plugins.
// The registrar breaks the import cycle between the plugin package and pluginapi/grpc/server.
func (env *Environment) SetAPIServerRegistrar(registrar APIServerRegistrar) {
env.apiServerRegistrar = registrar
}
// Performs a full scan of the given path.
//
// This function will return info for all subdirectories that appear to be plugins (i.e. all
// subdirectories containing plugin manifest files, regardless of whether they could actually be
// parsed).
//
// Plugins are found non-recursively and paths beginning with a dot are always ignored.
func scanSearchPath(path string) ([]*model.BundleInfo, error) {
files, err := os.ReadDir(path)
if err != nil {
return nil, err
}
var ret []*model.BundleInfo
for _, file := range files {
if !file.IsDir() || file.Name()[0] == '.' {
continue
}
info := model.BundleInfoForPath(filepath.Join(path, file.Name()))
if info.Manifest != nil {
ret = append(ret, info)
}
}
return ret, nil
}
// Returns a list of all plugins within the environment.
func (env *Environment) Available() ([]*model.BundleInfo, error) {
return scanSearchPath(env.pluginDir)
}
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
// Returns a list of prepackaged plugins available in the local prepackaged_plugins folder,
// excluding those in transition out of being prepackaged.
//
MM-19606- Rework Prepackaged Plugins (#13449) * MM-19609 - Add new prepackage configuration settings (#13062) * Add signatures to the prepackaged plugins (#13138) * MM-19612 - Support querying local plugin marketplace when upst… (#13250) * MM-19612 - Support querying local plugin marketplace when upstream unavailable or disabled * Update translations file * Fixed comment * Updated to check EnableRemoteMarketplace setting and LocalOnly to get marketplace plugins * Fixed unit tests * Tests cleanup code * Removed unused error message * Updated tests * MM-19614- Updated Marketplace Service error id (#13388) * [MM-19610] Consume prepackaged plugins (#13005) * consume prepackaged plugins into memory * missing i18n * remove spurious .gitignore changes * return on failure to install prepackged plugins * cleanup * s/plugins/availablePlugins * whitespace * don't return extractDir when not needed * s/plug/plugin * error on icon, cleanup * update armored version of testplugin signature * honour AutomaticPrepackagedPlugins * document getPrepackagedPlugin * MM-19613 - Include prepackaged plugins in marketplace results (#13433) * Added prepackaged plugins to marketplace results * PR Feedback * PR Feedback * Update error where definition * Removing unnecessary var declaration * Updated comments * MM-21263 - Use EnableRemoteMarketplace in marketplace install… (#13438) * MM-21263 - Use EnableRemoteMarketplace in marketplace install endpoint * Call updateConfig before calling NewServer in TestHelper * Added translations * PR feedback * Translations * Feedback * s/helpers.go/download.go * Converging env.PrepackagedPlugins * Initial PR feedback * Ordered imports properly * Updated DownloadURL to return slice of bytes * Fixed method typo * Fixed logging * Added read lock for prepackaged plugins list * PR Feedback * Added condition to only install prepackaged plugin if it was previously enabled * Linting * Updated to check plugin state in config * Closing filereader * Only add local label if remote marketplace is enabled * Updated local tag description * Fixed tests Co-authored-by: Ali Farooq <ali.farooq0@pm.me> Co-authored-by: Shota Gvinepadze <wineson@gmail.com> Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
2020-01-15 13:38:55 -05:00
// The list content is immutable and should not be modified.
func (env *Environment) PrepackagedPlugins() []*PrepackagedPlugin {
env.prepackagedPluginsLock.RLock()
defer env.prepackagedPluginsLock.RUnlock()
return env.prepackagedPlugins
}
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
// TransitionallyPrepackagedPlugins returns a list of plugins transitionally prepackaged in the
// local prepackaged_plugins folder.
//
// The list content is immutable and should not be modified.
func (env *Environment) TransitionallyPrepackagedPlugins() []*PrepackagedPlugin {
env.prepackagedPluginsLock.RLock()
defer env.prepackagedPluginsLock.RUnlock()
return env.transitionallyPrepackagedPlugins
}
// ClearTransitionallyPrepackagedPlugins clears the list of plugins transitionally prepackaged
// in the local prepackaged_plugins folder.
func (env *Environment) ClearTransitionallyPrepackagedPlugins() {
env.prepackagedPluginsLock.RLock()
defer env.prepackagedPluginsLock.RUnlock()
env.transitionallyPrepackagedPlugins = nil
}
// Returns a list of all currently active plugins within the environment.
// The returned list should not be modified.
func (env *Environment) Active() []*model.BundleInfo {
activePlugins := []*model.BundleInfo{}
env.registeredPlugins.Range(func(key, value any) bool {
plugin := value.(registeredPlugin)
if env.IsActive(plugin.BundleInfo.Manifest.Id) {
activePlugins = append(activePlugins, plugin.BundleInfo)
}
return true
})
return activePlugins
}
// IsActive returns true if the plugin with the given id is active.
func (env *Environment) IsActive(id string) bool {
return env.GetPluginState(id) == model.PluginStateRunning
}
func (env *Environment) SetPluginError(id string, err string) {
if rp, ok := env.registeredPlugins.Load(id); ok {
p := rp.(registeredPlugin)
p.Error = err
env.registeredPlugins.Store(id, p)
}
}
func (env *Environment) getPluginError(id string) string {
if rp, ok := env.registeredPlugins.Load(id); ok {
return rp.(registeredPlugin).Error
}
return ""
}
// GetPluginState returns the current state of a plugin (disabled, running, or error)
func (env *Environment) GetPluginState(id string) int {
rp, ok := env.registeredPlugins.Load(id)
if !ok {
return model.PluginStateNotRunning
}
return rp.(registeredPlugin).State
}
// setPluginState sets the current state of a plugin (disabled, running, or error)
func (env *Environment) setPluginState(id string, state int) {
if rp, ok := env.registeredPlugins.Load(id); ok {
p := rp.(registeredPlugin)
p.State = state
env.registeredPlugins.Store(id, p)
}
}
// setPluginSupervisor records the supervisor for a registered plugin.
func (env *Environment) setPluginSupervisor(id string, supervisor *supervisor) {
if rp, ok := env.registeredPlugins.Load(id); ok {
p := rp.(registeredPlugin)
p.supervisor = supervisor
env.registeredPlugins.Store(id, p)
}
}
// PublicFilesPath returns a path and true if the plugin with the given id is active.
// It returns an empty string and false if the path is not set or invalid
func (env *Environment) PublicFilesPath(id string) (string, error) {
if !env.IsActive(id) {
return "", fmt.Errorf("plugin not found: %v", id)
}
return filepath.Join(env.pluginDir, id, "public"), nil
}
// Statuses returns a list of plugin statuses representing the state of every plugin
func (env *Environment) Statuses() (model.PluginStatuses, error) {
plugins, err := env.Available()
if err != nil {
return nil, errors.Wrap(err, "unable to get plugin statuses")
}
pluginStatuses := make(model.PluginStatuses, 0, len(plugins))
for _, plugin := range plugins {
// For now we don't handle bad manifests, we should
if plugin.Manifest == nil {
continue
}
pluginState := env.GetPluginState(plugin.Manifest.Id)
status := &model.PluginStatus{
PluginId: plugin.Manifest.Id,
PluginPath: filepath.Dir(plugin.ManifestPath),
State: pluginState,
Error: env.getPluginError(plugin.Manifest.Id),
Name: plugin.Manifest.Name,
Description: plugin.Manifest.Description,
Version: plugin.Manifest.Version,
}
pluginStatuses = append(pluginStatuses, status)
}
return pluginStatuses, nil
}
MM-17023: Plugin Marketplace (#12183) * MM-17149 - Extend config.json for marketplace settings (#11933) * MM-17149 - Extend config.json for marketplace settings * Renamed MarketplaceUrl, tracking default marketplace url * Added EnableMarketplace to the client config * Revert "Added EnableMarketplace to the client config" This reverts commit 0f982c4c661c2cd9bb96264e9a01a2363c40d9c5. * MM-17149 - Added EnableMarketplace to the client config (#11958) * Added EnableMarketplace to the client config * Moved EnableMarketplace setting out of limited client configuration * MM-17150, MM-17545, MM-18100 - Implement GET /api/v4/plugins/m… (#11977) * MM-17150 - Implement GET /api/v4/plugins/marketplace proxying upstream MM-17545 - Merge locally installed plugins into GET /api/v4/plugins/marketplace * Replaced MarketplacePluginState with Installed * Setting InstalledVersion instead of Installed * marketplace client setting per_page if non zero * Creating insecure client for marketplace url * Fixed trailing slash for default marketplace url * Adding filtering * Fixed function names * Renamed Manifest() to GetManifest(), added godoc for BaseMarketplacePlugin * Handling plugin.ErrNotFound correctly * Checking err == nil instead when a plugin is installed * MM-18450 - Local-only plugin search (#12152) * MM-17846: plugin icons (#12157) * MM-17846: add support for plugin icons Extend the model definitions to support plugin icons from the marketplace. * s/IconURL/IconData * MM-18475 - Converge on snake_case responses from the marketplace (#12179) * MM-18520 - MM-Server should forward server version to marketplace server (#12181) * Renamed request to filter client4.GetMarketplacePlugins * Renamed request to filter * Guarding against bad marketplace server response
2019-09-17 15:02:26 -04:00
// GetManifest returns a manifest for a given pluginId.
// Returns ErrNotFound if plugin is not found.
func (env *Environment) GetManifest(pluginId string) (*model.Manifest, error) {
MM-17023: Plugin Marketplace (#12183) * MM-17149 - Extend config.json for marketplace settings (#11933) * MM-17149 - Extend config.json for marketplace settings * Renamed MarketplaceUrl, tracking default marketplace url * Added EnableMarketplace to the client config * Revert "Added EnableMarketplace to the client config" This reverts commit 0f982c4c661c2cd9bb96264e9a01a2363c40d9c5. * MM-17149 - Added EnableMarketplace to the client config (#11958) * Added EnableMarketplace to the client config * Moved EnableMarketplace setting out of limited client configuration * MM-17150, MM-17545, MM-18100 - Implement GET /api/v4/plugins/m… (#11977) * MM-17150 - Implement GET /api/v4/plugins/marketplace proxying upstream MM-17545 - Merge locally installed plugins into GET /api/v4/plugins/marketplace * Replaced MarketplacePluginState with Installed * Setting InstalledVersion instead of Installed * marketplace client setting per_page if non zero * Creating insecure client for marketplace url * Fixed trailing slash for default marketplace url * Adding filtering * Fixed function names * Renamed Manifest() to GetManifest(), added godoc for BaseMarketplacePlugin * Handling plugin.ErrNotFound correctly * Checking err == nil instead when a plugin is installed * MM-18450 - Local-only plugin search (#12152) * MM-17846: plugin icons (#12157) * MM-17846: add support for plugin icons Extend the model definitions to support plugin icons from the marketplace. * s/IconURL/IconData * MM-18475 - Converge on snake_case responses from the marketplace (#12179) * MM-18520 - MM-Server should forward server version to marketplace server (#12181) * Renamed request to filter client4.GetMarketplacePlugins * Renamed request to filter * Guarding against bad marketplace server response
2019-09-17 15:02:26 -04:00
plugins, err := env.Available()
if err != nil {
return nil, errors.Wrap(err, "unable to get plugin statuses")
}
for _, plugin := range plugins {
if plugin.Manifest != nil && plugin.Manifest.Id == pluginId {
MM-17023: Plugin Marketplace (#12183) * MM-17149 - Extend config.json for marketplace settings (#11933) * MM-17149 - Extend config.json for marketplace settings * Renamed MarketplaceUrl, tracking default marketplace url * Added EnableMarketplace to the client config * Revert "Added EnableMarketplace to the client config" This reverts commit 0f982c4c661c2cd9bb96264e9a01a2363c40d9c5. * MM-17149 - Added EnableMarketplace to the client config (#11958) * Added EnableMarketplace to the client config * Moved EnableMarketplace setting out of limited client configuration * MM-17150, MM-17545, MM-18100 - Implement GET /api/v4/plugins/m… (#11977) * MM-17150 - Implement GET /api/v4/plugins/marketplace proxying upstream MM-17545 - Merge locally installed plugins into GET /api/v4/plugins/marketplace * Replaced MarketplacePluginState with Installed * Setting InstalledVersion instead of Installed * marketplace client setting per_page if non zero * Creating insecure client for marketplace url * Fixed trailing slash for default marketplace url * Adding filtering * Fixed function names * Renamed Manifest() to GetManifest(), added godoc for BaseMarketplacePlugin * Handling plugin.ErrNotFound correctly * Checking err == nil instead when a plugin is installed * MM-18450 - Local-only plugin search (#12152) * MM-17846: plugin icons (#12157) * MM-17846: add support for plugin icons Extend the model definitions to support plugin icons from the marketplace. * s/IconURL/IconData * MM-18475 - Converge on snake_case responses from the marketplace (#12179) * MM-18520 - MM-Server should forward server version to marketplace server (#12181) * Renamed request to filter client4.GetMarketplacePlugins * Renamed request to filter * Guarding against bad marketplace server response
2019-09-17 15:02:26 -04:00
return plugin.Manifest, nil
}
}
return nil, ErrNotFound
}
func checkMinServerVersion(pluginInfo *model.BundleInfo) error {
if pluginInfo.Manifest.MinServerVersion == "" {
return nil
}
fulfilled, err := pluginInfo.Manifest.MeetMinServerVersion(model.CurrentVersion)
if err != nil {
return fmt.Errorf("%v: %v", err.Error(), pluginInfo.Manifest.Id)
}
if !fulfilled {
return fmt.Errorf("plugin requires Mattermost %v: %v", pluginInfo.Manifest.MinServerVersion, pluginInfo.Manifest.Id)
}
return nil
}
func (env *Environment) startPluginServer(pluginInfo *model.BundleInfo, opts ...func(*supervisor, *plugin.ClientConfig) error) error {
apiImpl := env.newAPIImpl(pluginInfo.Manifest)
sup, err := newSupervisor(pluginInfo, apiImpl, env.dbDriver, env.logger, env.metrics, opts...)
if err != nil {
return errors.Wrapf(err, "unable to start plugin: %v", pluginInfo.Manifest.Id)
}
// We pre-emptively set the state to running to prevent re-entrancy issues.
// The plugin's OnActivate hook can in-turn call UpdateConfiguration
// which again calls this method. This method is guarded against multiple calls,
// but fails if it is called recursively.
//
// Therefore, setting the state to running prevents this from happening,
// and in case there is an error, the defer clause will set the proper state anyways.
env.setPluginState(pluginInfo.Manifest.Id, model.PluginStateRunning)
// For Python plugins in Phase 5, hooks are nil (hook dispatch lands in Phase 7).
// We still set the supervisor so health checks work, but skip OnActivate.
if sup.Hooks() == nil {
env.logger.Info("Python plugin activated without hooks (Phase 5 supervision mode)",
mlog.String("plugin_id", pluginInfo.Manifest.Id))
env.setPluginSupervisor(pluginInfo.Manifest.Id, sup)
return nil
}
if err := sup.Hooks().OnActivate(); err != nil {
sup.Shutdown()
return err
}
env.setPluginSupervisor(pluginInfo.Manifest.Id, sup)
return nil
}
func (env *Environment) Activate(id string) (manifest *model.Manifest, activated bool, reterr error) {
defer func() {
if reterr != nil {
env.SetPluginError(id, reterr.Error())
} else {
env.SetPluginError(id, "")
}
}()
// Check if we are already active
if env.IsActive(id) {
return nil, false, nil
}
plugins, err := env.Available()
if err != nil {
return nil, false, err
}
var pluginInfo *model.BundleInfo
for _, p := range plugins {
if p.Manifest != nil && p.Manifest.Id == id {
if pluginInfo != nil {
return nil, false, fmt.Errorf("multiple plugins found: %v", id)
}
pluginInfo = p
}
}
if pluginInfo == nil {
return nil, false, fmt.Errorf("plugin not found: %v", id)
}
rp := newRegisteredPlugin(pluginInfo)
env.registeredPlugins.Store(id, rp)
defer func() {
if reterr == nil {
env.setPluginState(id, model.PluginStateRunning)
} else {
env.setPluginState(id, model.PluginStateFailedToStart)
}
}()
err = checkMinServerVersion(pluginInfo)
if err != nil {
return nil, false, err
}
componentActivated := false
if pluginInfo.Manifest.HasWebapp() {
var updatedManifest *model.Manifest
updatedManifest, err = env.UnpackWebappBundle(id)
if err != nil {
return nil, false, errors.Wrapf(err, "unable to generate webapp bundle: %v", id)
}
pluginInfo.Manifest.Webapp.BundleHash = updatedManifest.Webapp.BundleHash
componentActivated = true
}
if pluginInfo.Manifest.HasServer() {
// Use WithCommandFromManifest which handles both Go (netrpc) and Python (gRPC) plugins
// For Python plugins, this also starts the gRPC PluginAPI server and passes
// the address to the subprocess via MATTERMOST_PLUGIN_API_TARGET env var
apiImpl := env.newAPIImpl(pluginInfo.Manifest)
// Pass the API server registrar (set via SetAPIServerRegistrar) to enable
// Python plugins to call back to Go API. If not set, Python plugins won't
// have API callback capability (nil registrar is handled gracefully).
err = env.startPluginServer(pluginInfo, WithCommandFromManifest(pluginInfo, apiImpl, env.apiServerRegistrar))
if err != nil {
return nil, false, err
}
componentActivated = true
}
if !componentActivated {
return nil, false, fmt.Errorf("unable to start plugin: must at least have a web app or server component")
}
mlog.Debug("Plugin activated", mlog.String("plugin_id", pluginInfo.Manifest.Id), mlog.String("version", pluginInfo.Manifest.Version))
return pluginInfo.Manifest, true, nil
}
// Reattach allows the server to bind to an existing plugin instance launched elsewhere.
func (env *Environment) Reattach(manifest *model.Manifest, pluginReattachConfig *model.PluginReattachConfig) (reterr error) {
id := manifest.Id
defer func() {
if reterr != nil {
env.SetPluginError(id, reterr.Error())
} else {
env.SetPluginError(id, "")
}
}()
// Check if we are already active
if env.IsActive(id) {
return nil
}
pluginInfo := &model.BundleInfo{
Path: "",
Manifest: manifest,
ManifestPath: "",
ManifestError: nil,
}
rp := newRegisteredPlugin(pluginInfo)
env.registeredPlugins.Store(id, rp)
defer func() {
if reterr == nil {
env.setPluginState(id, model.PluginStateRunning)
} else {
env.setPluginState(id, model.PluginStateFailedToStart)
}
}()
err := checkMinServerVersion(pluginInfo)
if err != nil {
return nil
}
if !pluginInfo.Manifest.HasServer() {
return errors.New("cannot reattach plugin without server component")
}
if pluginInfo.Manifest.HasWebapp() {
env.logger.Warn("Ignoring webapp for reattached plugin", mlog.String("plugin_id", id))
}
err = env.startPluginServer(pluginInfo, WithReattachConfig(pluginReattachConfig))
if err != nil {
return nil
}
mlog.Debug("Plugin reattached", mlog.String("plugin_id", pluginInfo.Manifest.Id), mlog.String("version", pluginInfo.Manifest.Version))
return nil
}
func (env *Environment) RemovePlugin(id string) {
if _, ok := env.registeredPlugins.Load(id); ok {
env.registeredPlugins.Delete(id)
}
}
// Deactivates the plugin with the given id.
func (env *Environment) Deactivate(id string) bool {
p, ok := env.registeredPlugins.Load(id)
if !ok {
return false
}
isActive := env.IsActive(id)
env.setPluginState(id, model.PluginStateNotRunning)
if !isActive {
return false
}
rp := p.(registeredPlugin)
if rp.supervisor != nil {
// Guard against nil hooks (Python plugins in Phase 5 don't have hooks yet)
if rp.supervisor.Hooks() != nil {
if err := rp.supervisor.Hooks().OnDeactivate(); err != nil {
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id), mlog.Err(err))
}
}
rp.supervisor.Shutdown()
}
return true
}
// RestartPlugin deactivates, then activates the plugin with the given id.
func (env *Environment) RestartPlugin(id string) error {
env.Deactivate(id)
_, _, err := env.Activate(id)
return err
}
// Shutdown deactivates all plugins and gracefully shuts down the environment.
func (env *Environment) Shutdown() {
env.TogglePluginHealthCheckJob(false)
var wg sync.WaitGroup
MM-49353: Setup intermediate signal handlers for plugin shutdown (#28653) There are several steps that a server runs through inside (*Server).Start after ch.initPlugins() till the signal handler is reached which handles the server shutdown procedure. The issue arises when the server is shutdown after ch.initPlugins() completes but before (*Server).Start finishes. In that case, the plugins are all started but they won't be shut down cleanly. To fix this edge-case, we set up an intermediate signal handler, which attaches itself as soon as ch.initPlugins is finished, allowing us to run the cleanup code in case the shutdown happens before (*Server).Start finishes. And when we do reach the main signal handler, we don't need this intermediate handler any more. So we just reset the handlers and use the main signal handler which takes care of shutting down the whole server. Note: This is still not 100% bug-proof because ch.initPlugins() will initialize _all_ plugins, and the shutdown can happen just after one plugin is initialized. To handle that case will require the need to set up signal handlers after every plugin init which feels like overkill to me. A sample flow diagram to visualize better: Edge-case server.Start() | ch.initPlugins() | <ctrl-c> | execute signal handler, os.Exit(1) Happy-path server.Start() | ch.initPlugins() | server.Start() finished | reset old signal handler | setup main signal handler | server runs on as usual until shutdown https://mattermost.atlassian.net/browse/MM-49353 ```release-note NONE ```
2025-01-21 00:45:19 -05:00
env.registeredPlugins.Range(func(_, value any) bool {
rp := value.(registeredPlugin)
if rp.supervisor == nil || !env.IsActive(rp.BundleInfo.Manifest.Id) {
return true
}
wg.Add(1)
done := make(chan bool)
go func() {
defer close(done)
// Guard against nil hooks (Python plugins in Phase 5 don't have hooks yet)
if rp.supervisor.Hooks() != nil {
if err := rp.supervisor.Hooks().OnDeactivate(); err != nil {
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id), mlog.Err(err))
}
}
}()
go func() {
defer wg.Done()
select {
case <-time.After(10 * time.Second):
env.logger.Warn("Plugin OnDeactivate() failed to complete in 10 seconds", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id))
case <-done:
}
rp.supervisor.Shutdown()
}()
return true
})
wg.Wait()
env.registeredPlugins.Range(func(key, value any) bool {
env.registeredPlugins.Delete(key)
return true
})
}
// UnpackWebappBundle unpacks webapp bundle for a given plugin id on disk.
func (env *Environment) UnpackWebappBundle(id string) (*model.Manifest, error) {
plugins, err := env.Available()
if err != nil {
return nil, errors.New("Unable to get available plugins")
}
var manifest *model.Manifest
for _, p := range plugins {
if p.Manifest != nil && p.Manifest.Id == id {
if manifest != nil {
return nil, fmt.Errorf("multiple plugins found: %v", id)
}
manifest = p.Manifest
}
}
if manifest == nil {
return nil, fmt.Errorf("plugin not found: %v", id)
}
bundlePath := filepath.Clean(manifest.Webapp.BundlePath)
if bundlePath == "" || bundlePath[0] == '.' {
return nil, fmt.Errorf("invalid webapp bundle path")
}
bundlePath = filepath.Join(env.pluginDir, id, bundlePath)
destinationPath := filepath.Join(env.webappPluginDir, id)
if err = os.RemoveAll(destinationPath); err != nil {
return nil, errors.Wrapf(err, "unable to remove old webapp bundle directory: %v", destinationPath)
}
if err = utils.CopyDir(filepath.Dir(bundlePath), destinationPath); err != nil {
return nil, errors.Wrapf(err, "unable to copy webapp bundle directory: %v", id)
}
sourceBundleFilepath := filepath.Join(destinationPath, filepath.Base(bundlePath))
sourceBundleFileContents, err := os.ReadFile(sourceBundleFilepath)
if err != nil {
return nil, errors.Wrapf(err, "unable to read webapp bundle: %v", id)
}
hash := fnv.New64a()
if _, err = hash.Write(sourceBundleFileContents); err != nil {
return nil, errors.Wrapf(err, "unable to generate hash for webapp bundle: %v", id)
}
manifest.Webapp.BundleHash = hash.Sum([]byte{})
if err = os.Rename(
sourceBundleFilepath,
filepath.Join(destinationPath, fmt.Sprintf("%s_%x_bundle.js", id, manifest.Webapp.BundleHash)),
); err != nil {
return nil, errors.Wrapf(err, "unable to rename webapp bundle: %v", id)
}
return manifest, nil
}
// HooksForPlugin returns the hooks API for the plugin with the given id.
//
// Consider using RunMultiPluginHook instead.
func (env *Environment) HooksForPlugin(id string) (Hooks, error) {
if p, ok := env.registeredPlugins.Load(id); ok {
rp := p.(registeredPlugin)
if rp.supervisor != nil && env.IsActive(id) {
return rp.supervisor.Hooks(), nil
}
}
return nil, fmt.Errorf("plugin not found: %v", id)
}
// RunMultiPluginHook invokes hookRunnerFunc for each active plugin that implements the given hookId.
//
// If hookRunnerFunc returns false, iteration will not continue. The iteration order among active
// plugins is not specified.
func (env *Environment) RunMultiPluginHook(hookRunnerFunc func(hooks Hooks, manifest *model.Manifest) bool, hookId int) {
startTime := time.Now()
env.registeredPlugins.Range(func(key, value any) bool {
rp := value.(registeredPlugin)
if rp.supervisor == nil || !rp.supervisor.Implements(hookId) || !env.IsActive(rp.BundleInfo.Manifest.Id) {
return true
}
hookStartTime := time.Now()
result := hookRunnerFunc(rp.supervisor.Hooks(), rp.BundleInfo.Manifest)
if env.metrics != nil {
elapsedTime := float64(time.Since(hookStartTime)) / float64(time.Second)
env.metrics.ObservePluginMultiHookIterationDuration(rp.BundleInfo.Manifest.Id, elapsedTime)
}
return result
})
if env.metrics != nil {
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
env.metrics.ObservePluginMultiHookDuration(elapsedTime)
}
}
// PerformHealthCheck uses the active plugin's supervisor to verify if the plugin has crashed.
func (env *Environment) PerformHealthCheck(id string) error {
p, ok := env.registeredPlugins.Load(id)
if !ok {
return nil
}
rp := p.(registeredPlugin)
sup := rp.supervisor
if sup == nil {
return nil
}
return sup.PerformHealthCheck()
}
MM-19606- Rework Prepackaged Plugins (#13449) * MM-19609 - Add new prepackage configuration settings (#13062) * Add signatures to the prepackaged plugins (#13138) * MM-19612 - Support querying local plugin marketplace when upst… (#13250) * MM-19612 - Support querying local plugin marketplace when upstream unavailable or disabled * Update translations file * Fixed comment * Updated to check EnableRemoteMarketplace setting and LocalOnly to get marketplace plugins * Fixed unit tests * Tests cleanup code * Removed unused error message * Updated tests * MM-19614- Updated Marketplace Service error id (#13388) * [MM-19610] Consume prepackaged plugins (#13005) * consume prepackaged plugins into memory * missing i18n * remove spurious .gitignore changes * return on failure to install prepackged plugins * cleanup * s/plugins/availablePlugins * whitespace * don't return extractDir when not needed * s/plug/plugin * error on icon, cleanup * update armored version of testplugin signature * honour AutomaticPrepackagedPlugins * document getPrepackagedPlugin * MM-19613 - Include prepackaged plugins in marketplace results (#13433) * Added prepackaged plugins to marketplace results * PR Feedback * PR Feedback * Update error where definition * Removing unnecessary var declaration * Updated comments * MM-21263 - Use EnableRemoteMarketplace in marketplace install… (#13438) * MM-21263 - Use EnableRemoteMarketplace in marketplace install endpoint * Call updateConfig before calling NewServer in TestHelper * Added translations * PR feedback * Translations * Feedback * s/helpers.go/download.go * Converging env.PrepackagedPlugins * Initial PR feedback * Ordered imports properly * Updated DownloadURL to return slice of bytes * Fixed method typo * Fixed logging * Added read lock for prepackaged plugins list * PR Feedback * Added condition to only install prepackaged plugin if it was previously enabled * Linting * Updated to check plugin state in config * Closing filereader * Only add local label if remote marketplace is enabled * Updated local tag description * Fixed tests Co-authored-by: Ali Farooq <ali.farooq0@pm.me> Co-authored-by: Shota Gvinepadze <wineson@gmail.com> Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
2020-01-15 13:38:55 -05:00
// SetPrepackagedPlugins saves prepackaged plugins in the environment.
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
func (env *Environment) SetPrepackagedPlugins(plugins, transitionalPlugins []*PrepackagedPlugin) {
MM-19606- Rework Prepackaged Plugins (#13449) * MM-19609 - Add new prepackage configuration settings (#13062) * Add signatures to the prepackaged plugins (#13138) * MM-19612 - Support querying local plugin marketplace when upst… (#13250) * MM-19612 - Support querying local plugin marketplace when upstream unavailable or disabled * Update translations file * Fixed comment * Updated to check EnableRemoteMarketplace setting and LocalOnly to get marketplace plugins * Fixed unit tests * Tests cleanup code * Removed unused error message * Updated tests * MM-19614- Updated Marketplace Service error id (#13388) * [MM-19610] Consume prepackaged plugins (#13005) * consume prepackaged plugins into memory * missing i18n * remove spurious .gitignore changes * return on failure to install prepackged plugins * cleanup * s/plugins/availablePlugins * whitespace * don't return extractDir when not needed * s/plug/plugin * error on icon, cleanup * update armored version of testplugin signature * honour AutomaticPrepackagedPlugins * document getPrepackagedPlugin * MM-19613 - Include prepackaged plugins in marketplace results (#13433) * Added prepackaged plugins to marketplace results * PR Feedback * PR Feedback * Update error where definition * Removing unnecessary var declaration * Updated comments * MM-21263 - Use EnableRemoteMarketplace in marketplace install… (#13438) * MM-21263 - Use EnableRemoteMarketplace in marketplace install endpoint * Call updateConfig before calling NewServer in TestHelper * Added translations * PR feedback * Translations * Feedback * s/helpers.go/download.go * Converging env.PrepackagedPlugins * Initial PR feedback * Ordered imports properly * Updated DownloadURL to return slice of bytes * Fixed method typo * Fixed logging * Added read lock for prepackaged plugins list * PR Feedback * Added condition to only install prepackaged plugin if it was previously enabled * Linting * Updated to check plugin state in config * Closing filereader * Only add local label if remote marketplace is enabled * Updated local tag description * Fixed tests Co-authored-by: Ali Farooq <ali.farooq0@pm.me> Co-authored-by: Shota Gvinepadze <wineson@gmail.com> Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
2020-01-15 13:38:55 -05:00
env.prepackagedPluginsLock.Lock()
env.prepackagedPlugins = plugins
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
env.transitionallyPrepackagedPlugins = transitionalPlugins
MM-19606- Rework Prepackaged Plugins (#13449) * MM-19609 - Add new prepackage configuration settings (#13062) * Add signatures to the prepackaged plugins (#13138) * MM-19612 - Support querying local plugin marketplace when upst… (#13250) * MM-19612 - Support querying local plugin marketplace when upstream unavailable or disabled * Update translations file * Fixed comment * Updated to check EnableRemoteMarketplace setting and LocalOnly to get marketplace plugins * Fixed unit tests * Tests cleanup code * Removed unused error message * Updated tests * MM-19614- Updated Marketplace Service error id (#13388) * [MM-19610] Consume prepackaged plugins (#13005) * consume prepackaged plugins into memory * missing i18n * remove spurious .gitignore changes * return on failure to install prepackged plugins * cleanup * s/plugins/availablePlugins * whitespace * don't return extractDir when not needed * s/plug/plugin * error on icon, cleanup * update armored version of testplugin signature * honour AutomaticPrepackagedPlugins * document getPrepackagedPlugin * MM-19613 - Include prepackaged plugins in marketplace results (#13433) * Added prepackaged plugins to marketplace results * PR Feedback * PR Feedback * Update error where definition * Removing unnecessary var declaration * Updated comments * MM-21263 - Use EnableRemoteMarketplace in marketplace install… (#13438) * MM-21263 - Use EnableRemoteMarketplace in marketplace install endpoint * Call updateConfig before calling NewServer in TestHelper * Added translations * PR feedback * Translations * Feedback * s/helpers.go/download.go * Converging env.PrepackagedPlugins * Initial PR feedback * Ordered imports properly * Updated DownloadURL to return slice of bytes * Fixed method typo * Fixed logging * Added read lock for prepackaged plugins list * PR Feedback * Added condition to only install prepackaged plugin if it was previously enabled * Linting * Updated to check plugin state in config * Closing filereader * Only add local label if remote marketplace is enabled * Updated local tag description * Fixed tests Co-authored-by: Ali Farooq <ali.farooq0@pm.me> Co-authored-by: Shota Gvinepadze <wineson@gmail.com> Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
2020-01-15 13:38:55 -05:00
env.prepackagedPluginsLock.Unlock()
}
func newRegisteredPlugin(bundle *model.BundleInfo) registeredPlugin {
state := model.PluginStateNotRunning
return registeredPlugin{State: state, BundleInfo: bundle}
}
// TogglePluginHealthCheckJob starts a new job if one is not running and is set to enabled, or kills an existing one if set to disabled.
func (env *Environment) TogglePluginHealthCheckJob(enable bool) {
// Config is set to enable. No job exists, start a new job.
if enable && env.pluginHealthCheckJob == nil {
mlog.Debug("Enabling plugin health check job", mlog.Duration("interval_s", HealthCheckInterval))
job := newPluginHealthCheckJob(env)
env.pluginHealthCheckJob = job
go job.run()
}
// Config is set to disable. Job exists, kill existing job.
if !enable && env.pluginHealthCheckJob != nil {
mlog.Debug("Disabling plugin health check job")
env.pluginHealthCheckJob.Cancel()
env.pluginHealthCheckJob = nil
}
}
// GetPluginHealthCheckJob returns the configured PluginHealthCheckJob, if any.
func (env *Environment) GetPluginHealthCheckJob() *PluginHealthCheckJob {
return env.pluginHealthCheckJob
}