2018-06-25 15:33:13 -04:00
|
|
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
|
|
|
// See LICENSE.txt for license information.
|
|
|
|
|
|
|
|
|
|
package plugin
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
2018-07-31 16:29:52 -04:00
|
|
|
"hash/fnv"
|
2018-07-31 10:44:44 -04:00
|
|
|
"os"
|
2018-06-25 15:33:13 -04:00
|
|
|
"path/filepath"
|
|
|
|
|
"sync"
|
2019-06-25 17:44:08 -04:00
|
|
|
"time"
|
2018-06-25 15:33:13 -04:00
|
|
|
|
2024-04-11 11:10:25 -04:00
|
|
|
plugin "github.com/hashicorp/go-plugin"
|
2021-01-07 12:12:43 -05:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
|
2023-06-11 01:24:35 -04:00
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
|
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
|
|
|
"github.com/mattermost/mattermost/server/public/utils"
|
2018-06-25 15:33:13 -04:00
|
|
|
)
|
|
|
|
|
|
2019-09-17 15:02:26 -04:00
|
|
|
var ErrNotFound = errors.New("Item not found")
|
|
|
|
|
|
2018-07-13 10:29:50 -04:00
|
|
|
type apiImplCreatorFunc func(*model.Manifest) API
|
2018-06-25 15:33:13 -04:00
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
// 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 {
|
2018-06-25 15:33:13 -04:00
|
|
|
BundleInfo *model.BundleInfo
|
2020-01-07 07:00:34 -05:00
|
|
|
State int
|
2022-07-21 10:55:57 -04:00
|
|
|
Error string
|
2018-07-13 10:29:50 -04:00
|
|
|
|
2020-03-31 20:20:22 -04:00
|
|
|
supervisor *supervisor
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
|
2020-01-15 13:38:55 -05:00
|
|
|
// PrepackagedPlugin is a plugin prepackaged with the server and found on startup.
|
|
|
|
|
type PrepackagedPlugin struct {
|
2025-06-24 14:11:02 -04:00
|
|
|
Path string
|
|
|
|
|
IconData string
|
|
|
|
|
Manifest *model.Manifest
|
|
|
|
|
SignaturePath string
|
2020-01-15 13:38:55 -05:00
|
|
|
}
|
|
|
|
|
|
2018-07-13 10:29:50 -04: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.
|
2018-06-25 15:33:13 -04:00
|
|
|
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
|
2024-04-16 09:23:26 -04:00
|
|
|
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
|
2026-01-20 09:46:34 -05:00
|
|
|
apiServerRegistrar APIServerRegistrar // For Python plugins to call back to Go API
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
|
2022-12-06 12:44:48 -05:00
|
|
|
func NewEnvironment(
|
|
|
|
|
newAPIImpl apiImplCreatorFunc,
|
2024-04-16 09:23:26 -04:00
|
|
|
dbDriver AppDriver,
|
2022-12-06 12:44:48 -05:00
|
|
|
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,
|
2023-05-09 12:30:02 -04:00
|
|
|
metrics metricsInterface,
|
2022-12-06 12:44:48 -05:00
|
|
|
) (*Environment, error) {
|
2018-06-25 15:33:13 -04:00
|
|
|
return &Environment{
|
|
|
|
|
logger: logger,
|
2020-02-14 15:47:43 -05:00
|
|
|
metrics: metrics,
|
2018-06-25 15:33:13 -04:00
|
|
|
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,
|
2018-06-25 15:33:13 -04:00
|
|
|
pluginDir: pluginDir,
|
|
|
|
|
webappPluginDir: webappPluginDir,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 09:46:34 -05:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-25 15:33:13 -04:00
|
|
|
// 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.
|
2018-07-13 10:29:50 -04:00
|
|
|
func scanSearchPath(path string) ([]*model.BundleInfo, error) {
|
2022-08-09 07:25:46 -04:00
|
|
|
files, err := os.ReadDir(path)
|
2018-06-25 15:33:13 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
var ret []*model.BundleInfo
|
|
|
|
|
for _, file := range files {
|
|
|
|
|
if !file.IsDir() || file.Name()[0] == '.' {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2020-10-07 03:23:59 -04:00
|
|
|
info := model.BundleInfoForPath(filepath.Join(path, file.Name()))
|
|
|
|
|
if info.Manifest != nil {
|
2018-06-25 15:33:13 -04:00
|
|
|
ret = append(ret, info)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ret, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns a list of all plugins within the environment.
|
|
|
|
|
func (env *Environment) Available() ([]*model.BundleInfo, error) {
|
2023-08-08 17:29:57 -04:00
|
|
|
return scanSearchPath(env.pluginDir)
|
2018-06-25 15:33:13 -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
|
|
|
// Returns a list of prepackaged plugins available in the local prepackaged_plugins folder,
|
|
|
|
|
// excluding those in transition out of being prepackaged.
|
|
|
|
|
//
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-25 15:33:13 -04:00
|
|
|
// Returns a list of all currently active plugins within the environment.
|
2020-01-23 22:30:35 -05:00
|
|
|
// The returned list should not be modified.
|
2018-06-25 15:33:13 -04:00
|
|
|
func (env *Environment) Active() []*model.BundleInfo {
|
|
|
|
|
activePlugins := []*model.BundleInfo{}
|
2022-07-05 02:46:50 -04:00
|
|
|
env.registeredPlugins.Range(func(key, value any) bool {
|
2020-01-23 22:30:35 -05:00
|
|
|
plugin := value.(registeredPlugin)
|
2019-06-25 17:44:08 -04:00
|
|
|
if env.IsActive(plugin.BundleInfo.Manifest.Id) {
|
2019-05-28 13:01:02 -04:00
|
|
|
activePlugins = append(activePlugins, plugin.BundleInfo)
|
|
|
|
|
}
|
2018-07-27 11:37:17 -04:00
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
})
|
2018-06-25 15:33:13 -04:00
|
|
|
|
|
|
|
|
return activePlugins
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-13 10:29:50 -04:00
|
|
|
// IsActive returns true if the plugin with the given id is active.
|
2018-06-25 15:33:13 -04:00
|
|
|
func (env *Environment) IsActive(id string) bool {
|
2019-06-25 17:44:08 -04:00
|
|
|
return env.GetPluginState(id) == model.PluginStateRunning
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-08 00:51:22 -04:00
|
|
|
func (env *Environment) SetPluginError(id string, err string) {
|
2022-07-21 10:55:57 -04:00
|
|
|
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 ""
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-23 22:30:35 -05:00
|
|
|
return rp.(registeredPlugin).State
|
2019-06-25 17:44:08 -04:00
|
|
|
}
|
|
|
|
|
|
2020-03-31 20:20:22 -04:00
|
|
|
// setPluginState sets the current state of a plugin (disabled, running, or error)
|
|
|
|
|
func (env *Environment) setPluginState(id string, state int) {
|
2019-06-25 17:44:08 -04:00
|
|
|
if rp, ok := env.registeredPlugins.Load(id); ok {
|
2020-01-23 22:30:35 -05:00
|
|
|
p := rp.(registeredPlugin)
|
|
|
|
|
p.State = state
|
|
|
|
|
env.registeredPlugins.Store(id, p)
|
2019-06-25 17:44:08 -04:00
|
|
|
}
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
|
2024-04-11 11:10:25 -04:00
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-05 10:35:51 -04:00
|
|
|
// 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) {
|
2020-03-31 20:20:22 -04:00
|
|
|
if !env.IsActive(id) {
|
2019-04-05 10:35:51 -04:00
|
|
|
return "", fmt.Errorf("plugin not found: %v", id)
|
|
|
|
|
}
|
|
|
|
|
return filepath.Join(env.pluginDir, id, "public"), nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-13 10:29:50 -04:00
|
|
|
// Statuses returns a list of plugin statuses representing the state of every plugin
|
2018-06-25 15:33:13 -04:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
pluginState := env.GetPluginState(plugin.Manifest.Id)
|
2018-06-25 15:33:13 -04:00
|
|
|
|
|
|
|
|
status := &model.PluginStatus{
|
|
|
|
|
PluginId: plugin.Manifest.Id,
|
|
|
|
|
PluginPath: filepath.Dir(plugin.ManifestPath),
|
|
|
|
|
State: pluginState,
|
2022-07-21 10:55:57 -04:00
|
|
|
Error: env.getPluginError(plugin.Manifest.Id),
|
2018-06-25 15:33:13 -04:00
|
|
|
Name: plugin.Manifest.Name,
|
|
|
|
|
Description: plugin.Manifest.Description,
|
|
|
|
|
Version: plugin.Manifest.Version,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pluginStatuses = append(pluginStatuses, status)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pluginStatuses, nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-17 15:02:26 -04:00
|
|
|
// GetManifest returns a manifest for a given pluginId.
|
|
|
|
|
// Returns ErrNotFound if plugin is not found.
|
2021-03-23 05:32:54 -04:00
|
|
|
func (env *Environment) GetManifest(pluginId string) (*model.Manifest, error) {
|
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 {
|
2021-03-23 05:32:54 -04:00
|
|
|
if plugin.Manifest != nil && plugin.Manifest.Id == pluginId {
|
2019-09-17 15:02:26 -04:00
|
|
|
return plugin.Manifest, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil, ErrNotFound
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-11 11:10:25 -04:00
|
|
|
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 {
|
2026-01-20 09:46:34 -05:00
|
|
|
apiImpl := env.newAPIImpl(pluginInfo.Manifest)
|
|
|
|
|
sup, err := newSupervisor(pluginInfo, apiImpl, env.dbDriver, env.logger, env.metrics, opts...)
|
2024-04-11 11:10:25 -04:00
|
|
|
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)
|
|
|
|
|
|
2026-01-16 14:49:01 -05:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-11 11:10:25 -04:00
|
|
|
if err := sup.Hooks().OnActivate(); err != nil {
|
|
|
|
|
sup.Shutdown()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
env.setPluginSupervisor(pluginInfo.Manifest.Id, sup)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-31 16:29:52 -04:00
|
|
|
func (env *Environment) Activate(id string) (manifest *model.Manifest, activated bool, reterr error) {
|
2022-07-21 10:55:57 -04:00
|
|
|
defer func() {
|
|
|
|
|
if reterr != nil {
|
2022-10-08 00:51:22 -04:00
|
|
|
env.SetPluginError(id, reterr.Error())
|
2022-07-21 10:55:57 -04:00
|
|
|
} else {
|
2022-10-08 00:51:22 -04:00
|
|
|
env.SetPluginError(id, "")
|
2022-07-21 10:55:57 -04:00
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2018-06-25 15:33:13 -04:00
|
|
|
// Check if we are already active
|
2019-06-25 17:44:08 -04:00
|
|
|
if env.IsActive(id) {
|
2018-07-31 16:29:52 -04:00
|
|
|
return nil, false, nil
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
plugins, err := env.Available()
|
|
|
|
|
if err != nil {
|
2018-07-31 16:29:52 -04:00
|
|
|
return nil, false, err
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
var pluginInfo *model.BundleInfo
|
|
|
|
|
for _, p := range plugins {
|
|
|
|
|
if p.Manifest != nil && p.Manifest.Id == id {
|
|
|
|
|
if pluginInfo != nil {
|
2018-07-31 16:29:52 -04:00
|
|
|
return nil, false, fmt.Errorf("multiple plugins found: %v", id)
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
pluginInfo = p
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if pluginInfo == nil {
|
2018-07-31 16:29:52 -04:00
|
|
|
return nil, false, fmt.Errorf("plugin not found: %v", id)
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
|
2020-03-31 20:20:22 -04:00
|
|
|
rp := newRegisteredPlugin(pluginInfo)
|
2020-01-23 22:30:35 -05:00
|
|
|
env.registeredPlugins.Store(id, rp)
|
2019-06-25 17:44:08 -04:00
|
|
|
|
2018-06-25 15:33:13 -04:00
|
|
|
defer func() {
|
|
|
|
|
if reterr == nil {
|
2020-03-31 20:20:22 -04:00
|
|
|
env.setPluginState(id, model.PluginStateRunning)
|
2018-06-25 15:33:13 -04:00
|
|
|
} else {
|
2020-03-31 20:20:22 -04:00
|
|
|
env.setPluginState(id, model.PluginStateFailedToStart)
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2024-04-11 11:10:25 -04:00
|
|
|
err = checkMinServerVersion(pluginInfo)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, false, err
|
2018-11-05 08:29:25 -05:00
|
|
|
}
|
|
|
|
|
|
2018-09-21 11:07:32 -04:00
|
|
|
componentActivated := false
|
|
|
|
|
|
|
|
|
|
if pluginInfo.Manifest.HasWebapp() {
|
2024-04-11 11:10:25 -04:00
|
|
|
var updatedManifest *model.Manifest
|
|
|
|
|
updatedManifest, err = env.UnpackWebappBundle(id)
|
2018-07-31 16:29:52 -04:00
|
|
|
if err != nil {
|
2019-08-22 15:17:47 -04:00
|
|
|
return nil, false, errors.Wrapf(err, "unable to generate webapp bundle: %v", id)
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
2019-08-22 15:17:47 -04:00
|
|
|
pluginInfo.Manifest.Webapp.BundleHash = updatedManifest.Webapp.BundleHash
|
2018-09-21 11:07:32 -04:00
|
|
|
|
|
|
|
|
componentActivated = true
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
|
2018-07-18 18:32:33 -04:00
|
|
|
if pluginInfo.Manifest.HasServer() {
|
2026-01-16 14:51:42 -05:00
|
|
|
// Use WithCommandFromManifest which handles both Go (netrpc) and Python (gRPC) plugins
|
2026-01-20 09:46:34 -05:00
|
|
|
// 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))
|
2018-06-25 15:33:13 -04:00
|
|
|
if err != nil {
|
2020-01-24 09:49:49 -05:00
|
|
|
return nil, false, err
|
|
|
|
|
}
|
2018-09-21 11:07:32 -04:00
|
|
|
componentActivated = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !componentActivated {
|
|
|
|
|
return nil, false, fmt.Errorf("unable to start plugin: must at least have a web app or server component")
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
|
2023-01-02 18:07:38 -05:00
|
|
|
mlog.Debug("Plugin activated", mlog.String("plugin_id", pluginInfo.Manifest.Id), mlog.String("version", pluginInfo.Manifest.Version))
|
|
|
|
|
|
2018-07-31 16:29:52 -04:00
|
|
|
return pluginInfo.Manifest, true, nil
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
|
2024-04-11 11:10:25 -04:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
func (env *Environment) RemovePlugin(id string) {
|
|
|
|
|
if _, ok := env.registeredPlugins.Load(id); ok {
|
|
|
|
|
env.registeredPlugins.Delete(id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-25 15:33:13 -04:00
|
|
|
// Deactivates the plugin with the given id.
|
2018-07-31 16:29:52 -04:00
|
|
|
func (env *Environment) Deactivate(id string) bool {
|
2019-06-25 17:44:08 -04:00
|
|
|
p, ok := env.registeredPlugins.Load(id)
|
2018-07-27 11:37:17 -04:00
|
|
|
if !ok {
|
2018-07-31 16:29:52 -04:00
|
|
|
return false
|
2018-07-27 11:37:17 -04:00
|
|
|
}
|
|
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
isActive := env.IsActive(id)
|
|
|
|
|
|
2020-03-31 20:20:22 -04:00
|
|
|
env.setPluginState(id, model.PluginStateNotRunning)
|
2018-07-27 11:37:17 -04:00
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
if !isActive {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-23 22:30:35 -05:00
|
|
|
rp := p.(registeredPlugin)
|
2019-06-25 17:44:08 -04:00
|
|
|
if rp.supervisor != nil {
|
2026-01-16 14:49:01 -05:00
|
|
|
// 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))
|
|
|
|
|
}
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
2019-06-25 17:44:08 -04:00
|
|
|
rp.supervisor.Shutdown()
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
2018-07-31 16:29:52 -04:00
|
|
|
|
|
|
|
|
return true
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
|
2019-05-09 16:08:31 -04:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-13 10:29:50 -04:00
|
|
|
// Shutdown deactivates all plugins and gracefully shuts down the environment.
|
2018-06-25 15:33:13 -04:00
|
|
|
func (env *Environment) Shutdown() {
|
2021-11-03 23:24:03 -04:00
|
|
|
env.TogglePluginHealthCheckJob(false)
|
2019-08-29 14:36:38 -04:00
|
|
|
|
2019-09-05 16:27:36 -04:00
|
|
|
var wg sync.WaitGroup
|
2025-01-21 00:45:19 -05:00
|
|
|
env.registeredPlugins.Range(func(_, value any) bool {
|
2020-01-23 22:30:35 -05:00
|
|
|
rp := value.(registeredPlugin)
|
2018-06-25 15:33:13 -04:00
|
|
|
|
2020-01-24 09:49:49 -05:00
|
|
|
if rp.supervisor == nil || !env.IsActive(rp.BundleInfo.Manifest.Id) {
|
2019-09-05 16:27:36 -04:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
|
|
|
|
|
done := make(chan bool)
|
|
|
|
|
go func() {
|
|
|
|
|
defer close(done)
|
2026-01-16 14:49:01 -05:00
|
|
|
// 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))
|
|
|
|
|
}
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
2019-09-05 16:27:36 -04:00
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-25 17:44:08 -04:00
|
|
|
rp.supervisor.Shutdown()
|
2019-09-05 16:27:36 -04:00
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
wg.Wait()
|
2018-07-27 11:37:17 -04:00
|
|
|
|
2022-07-05 02:46:50 -04:00
|
|
|
env.registeredPlugins.Range(func(key, value any) bool {
|
2019-06-25 17:44:08 -04:00
|
|
|
env.registeredPlugins.Delete(key)
|
2018-07-27 11:37:17 -04:00
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
})
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
|
2019-08-22 15:17:47 -04:00
|
|
|
// 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))
|
|
|
|
|
|
2022-08-09 07:25:46 -04:00
|
|
|
sourceBundleFileContents, err := os.ReadFile(sourceBundleFilepath)
|
2019-08-22 15:17:47 -04:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-13 10:29:50 -04:00
|
|
|
// HooksForPlugin returns the hooks API for the plugin with the given id.
|
|
|
|
|
//
|
|
|
|
|
// Consider using RunMultiPluginHook instead.
|
2018-06-25 15:33:13 -04:00
|
|
|
func (env *Environment) HooksForPlugin(id string) (Hooks, error) {
|
2019-06-25 17:44:08 -04:00
|
|
|
if p, ok := env.registeredPlugins.Load(id); ok {
|
2020-01-23 22:30:35 -05:00
|
|
|
rp := p.(registeredPlugin)
|
2020-01-24 09:49:49 -05:00
|
|
|
if rp.supervisor != nil && env.IsActive(id) {
|
2019-06-25 17:44:08 -04:00
|
|
|
return rp.supervisor.Hooks(), nil
|
2018-07-27 11:37:17 -04:00
|
|
|
}
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("plugin not found: %v", id)
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-24 09:49:49 -05:00
|
|
|
// RunMultiPluginHook invokes hookRunnerFunc for each active plugin that implements the given hookId.
|
2018-07-13 10:29:50 -04:00
|
|
|
//
|
|
|
|
|
// If hookRunnerFunc returns false, iteration will not continue. The iteration order among active
|
|
|
|
|
// plugins is not specified.
|
2024-11-13 05:20:39 -05:00
|
|
|
func (env *Environment) RunMultiPluginHook(hookRunnerFunc func(hooks Hooks, manifest *model.Manifest) bool, hookId int) {
|
2020-02-14 15:47:43 -05:00
|
|
|
startTime := time.Now()
|
|
|
|
|
|
2022-07-05 02:46:50 -04:00
|
|
|
env.registeredPlugins.Range(func(key, value any) bool {
|
2020-01-23 22:30:35 -05:00
|
|
|
rp := value.(registeredPlugin)
|
2018-06-25 15:33:13 -04:00
|
|
|
|
2021-03-23 05:32:54 -04:00
|
|
|
if rp.supervisor == nil || !rp.supervisor.Implements(hookId) || !env.IsActive(rp.BundleInfo.Manifest.Id) {
|
2018-07-27 11:37:17 -04:00
|
|
|
return true
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
2018-07-27 11:37:17 -04:00
|
|
|
|
2020-02-14 15:47:43 -05:00
|
|
|
hookStartTime := time.Now()
|
2024-11-13 05:20:39 -05:00
|
|
|
result := hookRunnerFunc(rp.supervisor.Hooks(), rp.BundleInfo.Manifest)
|
2020-02-14 15:47:43 -05:00
|
|
|
|
|
|
|
|
if env.metrics != nil {
|
|
|
|
|
elapsedTime := float64(time.Since(hookStartTime)) / float64(time.Second)
|
|
|
|
|
env.metrics.ObservePluginMultiHookIterationDuration(rp.BundleInfo.Manifest.Id, elapsedTime)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
2018-07-27 11:37:17 -04:00
|
|
|
})
|
2020-02-14 15:47:43 -05:00
|
|
|
|
|
|
|
|
if env.metrics != nil {
|
|
|
|
|
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
|
|
|
|
|
env.metrics.ObservePluginMultiHookDuration(elapsedTime)
|
|
|
|
|
}
|
2018-06-25 15:33:13 -04:00
|
|
|
}
|
2019-06-25 17:44:08 -04:00
|
|
|
|
2020-10-02 04:02:58 -04:00
|
|
|
// PerformHealthCheck uses the active plugin's supervisor to verify if the plugin has crashed.
|
|
|
|
|
func (env *Environment) PerformHealthCheck(id string) error {
|
2020-03-31 20:20:22 -04:00
|
|
|
p, ok := env.registeredPlugins.Load(id)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
rp := p.(registeredPlugin)
|
|
|
|
|
|
|
|
|
|
sup := rp.supervisor
|
|
|
|
|
if sup == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return sup.PerformHealthCheck()
|
|
|
|
|
}
|
|
|
|
|
|
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) {
|
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
|
2020-01-15 13:38:55 -05:00
|
|
|
env.prepackagedPluginsLock.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-23 22:30:35 -05:00
|
|
|
func newRegisteredPlugin(bundle *model.BundleInfo) registeredPlugin {
|
2019-06-25 17:44:08 -04:00
|
|
|
state := model.PluginStateNotRunning
|
2020-03-31 20:20:22 -04:00
|
|
|
return registeredPlugin{State: state, BundleInfo: bundle}
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-03 23:24:03 -04:00
|
|
|
// 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) {
|
2020-03-31 20:20:22 -04:00
|
|
|
// Config is set to enable. No job exists, start a new job.
|
|
|
|
|
if enable && env.pluginHealthCheckJob == nil {
|
2021-01-04 01:02:29 -05:00
|
|
|
mlog.Debug("Enabling plugin health check job", mlog.Duration("interval_s", HealthCheckInterval))
|
2020-03-31 20:20:22 -04:00
|
|
|
|
|
|
|
|
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
|
2019-06-25 17:44:08 -04:00
|
|
|
}
|