mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06: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>
This commit is contained in:
@@ -1210,7 +1210,7 @@ func TestGetPrepackagedPluginInMarketplace(t *testing.T) {
|
||||
}
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{prepackagePlugin})
|
||||
env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{prepackagePlugin}, nil)
|
||||
|
||||
t.Run("get remote and prepackaged plugins", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
@@ -1263,7 +1263,7 @@ func TestGetPrepackagedPluginInMarketplace(t *testing.T) {
|
||||
}
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{newerPrepackagePlugin})
|
||||
env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{newerPrepackagePlugin}, nil)
|
||||
|
||||
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -44,11 +44,12 @@ type Channels struct {
|
||||
|
||||
postActionCookieSecret []byte
|
||||
|
||||
pluginCommandsLock sync.RWMutex
|
||||
pluginCommands []*PluginCommand
|
||||
pluginsLock sync.RWMutex
|
||||
pluginsEnvironment *plugin.Environment
|
||||
pluginConfigListenerID string
|
||||
pluginCommandsLock sync.RWMutex
|
||||
pluginCommands []*PluginCommand
|
||||
pluginsLock sync.RWMutex
|
||||
pluginsEnvironment *plugin.Environment
|
||||
pluginConfigListenerID string
|
||||
pluginClusterLeaderListenerID string
|
||||
|
||||
productCommandsLock sync.RWMutex
|
||||
productCommands []*ProductCommand
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -27,7 +28,6 @@ import (
|
||||
"github.com/mattermost/mattermost/server/v8/channels/product"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/services/marketplace"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/shared/filestore"
|
||||
)
|
||||
|
||||
// prepackagedPluginsDir is the hard-coded folder name where prepackaged plugins are bundled
|
||||
@@ -253,13 +253,13 @@ func (ch *Channels) initPlugins(c *request.Context, pluginDir, webappPluginDir s
|
||||
ch.srv.Log().Error("Failed to sync plugins from the file store", mlog.Err(err))
|
||||
}
|
||||
|
||||
plugins := ch.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
pluginsEnvironment = ch.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
ch.srv.Log().Info("Plugins environment not found, server is likely shutting down")
|
||||
return
|
||||
if err := ch.processPrepackagedPlugins(prepackagedPluginsDir); err != nil {
|
||||
ch.srv.Log().Error("Failed to process prepackaged plugins", mlog.Err(err))
|
||||
}
|
||||
pluginsEnvironment.SetPrepackagedPlugins(plugins)
|
||||
ch.pluginClusterLeaderListenerID = ch.srv.AddClusterLeaderChangedListener(func() {
|
||||
ch.persistTransitionallyPrepackagedPlugins()
|
||||
})
|
||||
ch.persistTransitionallyPrepackagedPlugins()
|
||||
|
||||
// Sync plugin active state when config changes. Also notify plugins.
|
||||
ch.pluginsLock.Lock()
|
||||
@@ -352,18 +352,22 @@ func (ch *Channels) syncPlugins() *model.AppError {
|
||||
}
|
||||
defer bundle.Close()
|
||||
|
||||
var signature filestore.ReadCloseSeeker
|
||||
if *ch.cfgSvc.Config().PluginSettings.RequirePluginSignature {
|
||||
signature, appErr = ch.srv.fileReader(plugin.signaturePath)
|
||||
signature, appErr := ch.srv.fileReader(plugin.signaturePath)
|
||||
if appErr != nil {
|
||||
logger.Error("Failed to open plugin signature from file store.", mlog.Err(appErr))
|
||||
return
|
||||
}
|
||||
defer signature.Close()
|
||||
|
||||
if appErr = ch.verifyPlugin(bundle, signature); appErr != nil {
|
||||
logger.Error("Failed to validate plugin signature", mlog.Err(appErr))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Syncing plugin from file store")
|
||||
if _, err := ch.installPluginLocally(bundle, signature, installPluginLocallyAlways); err != nil && err.Id != "app.plugin.skip_installation.app_error" {
|
||||
if _, err := ch.installPluginLocally(bundle, installPluginLocallyAlways); err != nil && err.Id != "app.plugin.skip_installation.app_error" {
|
||||
logger.Error("Failed to sync plugin from file store", mlog.Err(err))
|
||||
}
|
||||
}(plugin)
|
||||
@@ -388,6 +392,8 @@ func (ch *Channels) ShutDownPlugins() {
|
||||
|
||||
ch.RemoveConfigListener(ch.pluginConfigListenerID)
|
||||
ch.pluginConfigListenerID = ""
|
||||
ch.srv.RemoveClusterLeaderChangedListener(ch.pluginClusterLeaderListenerID)
|
||||
ch.pluginClusterLeaderListenerID = ""
|
||||
|
||||
// Acquiring lock manually before cleaning up PluginsEnvironment.
|
||||
ch.pluginsLock.Lock()
|
||||
@@ -911,26 +917,48 @@ func (ch *Channels) getPluginsFromFilePaths(fileStorePaths []string) map[string]
|
||||
return pluginSignaturePathMap
|
||||
}
|
||||
|
||||
func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.PrepackagedPlugin {
|
||||
prepackagedPluginsDir, found := fileutils.FindDir(pluginsDir)
|
||||
// processPrepackagedPlugins processes the plugins prepackaged with this server in the
|
||||
// prepackaged_plugins directory.
|
||||
//
|
||||
// If enabled, prepackaged plugins are installed or upgraded locally. A list of transitionally
|
||||
// prepackaged plugins is also collected for later persistence to the filestore.
|
||||
func (ch *Channels) processPrepackagedPlugins(prepackagedPluginsDir string) error {
|
||||
prepackagedPluginsPath, found := fileutils.FindDir(prepackagedPluginsDir)
|
||||
if !found {
|
||||
ch.srv.Log().Debug("No prepackaged plugins directory found")
|
||||
return nil
|
||||
}
|
||||
|
||||
ch.srv.Log().Debug("Processing prepackaged plugins in directory", mlog.String("path", prepackagedPluginsPath))
|
||||
|
||||
var fileStorePaths []string
|
||||
err := filepath.Walk(prepackagedPluginsDir, func(walkPath string, info os.FileInfo, err error) error {
|
||||
err := filepath.Walk(prepackagedPluginsPath, func(walkPath string, info os.FileInfo, err error) error {
|
||||
fileStorePaths = append(fileStorePaths, walkPath)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
ch.srv.Log().Error("Failed to walk prepackaged plugins", mlog.Err(err))
|
||||
return nil
|
||||
return errors.Wrap(err, "failed to walk prepackaged plugins")
|
||||
}
|
||||
|
||||
pluginSignaturePathMap := ch.getPluginsFromFilePaths(fileStorePaths)
|
||||
plugins := make([]*plugin.PrepackagedPlugin, 0, len(pluginSignaturePathMap))
|
||||
prepackagedPlugins := make(chan *plugin.PrepackagedPlugin, len(pluginSignaturePathMap))
|
||||
plugins := make(chan *plugin.PrepackagedPlugin, len(pluginSignaturePathMap))
|
||||
|
||||
// Before processing any prepackaged plugins, take a snapshot of the available manifests
|
||||
// to decide what was synced from the filestore.
|
||||
pluginsEnvironment := ch.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return errors.New("pluginsEnvironment is nil")
|
||||
}
|
||||
|
||||
availablePlugins, err := pluginsEnvironment.Available()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list available plugins")
|
||||
}
|
||||
|
||||
availablePluginsMap := make(map[string]*model.BundleInfo, len(availablePlugins))
|
||||
for _, bundleInfo := range availablePlugins {
|
||||
availablePluginsMap[bundleInfo.Manifest.Id] = bundleInfo
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, psPath := range pluginSignaturePathMap {
|
||||
@@ -946,18 +974,29 @@ func (ch *Channels) processPrepackagedPlugins(pluginsDir string) []*plugin.Prepa
|
||||
ch.srv.Log().Error("Failed to install prepackaged plugin", mlog.String("bundle_path", psPath.bundlePath), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
prepackagedPlugins <- p
|
||||
|
||||
plugins <- p
|
||||
}(psPath)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(prepackagedPlugins)
|
||||
close(plugins)
|
||||
|
||||
for p := range prepackagedPlugins {
|
||||
plugins = append(plugins, p)
|
||||
prepackagedPlugins := make([]*plugin.PrepackagedPlugin, 0, len(pluginSignaturePathMap))
|
||||
transitionallyPrepackagedPlugins := make([]*plugin.PrepackagedPlugin, 0)
|
||||
for p := range plugins {
|
||||
if ch.pluginIsTransitionallyPrepackaged(p.Manifest.Id) {
|
||||
if ch.shouldPersistTransitionallyPrepackagedPlugin(availablePluginsMap, p) {
|
||||
transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, p)
|
||||
}
|
||||
} else {
|
||||
prepackagedPlugins = append(prepackagedPlugins, p)
|
||||
}
|
||||
}
|
||||
|
||||
return plugins
|
||||
pluginsEnvironment.SetPrepackagedPlugins(prepackagedPlugins, transitionallyPrepackagedPlugins)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processPrepackagedPlugin will return the prepackaged plugin metadata and will also
|
||||
@@ -984,25 +1023,172 @@ func (ch *Channels) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*
|
||||
return nil, errors.Wrapf(err, "Failed to get prepackaged plugin %s", pluginPath.bundlePath)
|
||||
}
|
||||
|
||||
logger = logger.With(mlog.String("plugin_id", plugin.Manifest.Id))
|
||||
|
||||
// Skip installing the plugin at all if automatic prepackaged plugins is disabled
|
||||
if !*ch.cfgSvc.Config().PluginSettings.AutomaticPrepackagedPlugins {
|
||||
logger.Info("Not installing prepackaged plugin: automatic prepackaged plugins disabled")
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
// Skip installing if the plugin is has not been previously enabled.
|
||||
pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[plugin.Manifest.Id]
|
||||
if pluginState == nil || !pluginState.Enable {
|
||||
logger.Info("Not installing prepackaged plugin: not previously enabled")
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
logger.Info("Installing prepackaged plugin")
|
||||
if _, err := ch.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil {
|
||||
if _, err := ch.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil && err.Id != "app.plugin.skip_installation.app_error" {
|
||||
return nil, errors.Wrapf(err, "Failed to install extracted prepackaged plugin %s", pluginPath.bundlePath)
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
var transitionallyPrepackagedPlugins = []string{
|
||||
"antivirus",
|
||||
"focalboard",
|
||||
"mattermost-autolink",
|
||||
"com.mattermost.aws-sns",
|
||||
"com.mattermost.plugin-channel-export",
|
||||
"com.mattermost.confluence",
|
||||
"com.mattermost.custom-attributes",
|
||||
"jenkins",
|
||||
"jitsi",
|
||||
"com.mattermost.plugin-todo",
|
||||
"com.mattermost.welcomebot",
|
||||
"com.mattermost.apps",
|
||||
}
|
||||
|
||||
// pluginIsTransitionallyPrepackaged identifies plugin ids that are currently prepackaged but
|
||||
// slated for future removal.
|
||||
func (ch *Channels) pluginIsTransitionallyPrepackaged(pluginID string) bool {
|
||||
for _, id := range transitionallyPrepackagedPlugins {
|
||||
if id == pluginID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// shouldPersistTransitionallyPrepackagedPlugin determines if a transitionally prepackaged plugin
|
||||
// should be persisted to the filestore, taking into account whether it's already enabled and
|
||||
// would improve on what's already in the filestore.
|
||||
func (ch *Channels) shouldPersistTransitionallyPrepackagedPlugin(availablePluginsMap map[string]*model.BundleInfo, p *plugin.PrepackagedPlugin) bool {
|
||||
logger := ch.srv.Log().With(mlog.String("plugin_id", p.Manifest.Id), mlog.String("prepackaged_version", p.Manifest.Version))
|
||||
|
||||
// Ignore the plugin altogether unless it was previously enabled.
|
||||
pluginState := ch.cfgSvc.Config().PluginSettings.PluginStates[p.Manifest.Id]
|
||||
if pluginState == nil || !pluginState.Enable {
|
||||
logger.Debug("Should not persist transitionally prepackaged plugin: not previously enabled")
|
||||
return false
|
||||
}
|
||||
|
||||
// Ignore the plugin if the same or newer version is already available
|
||||
// (having previously synced from the filestore).
|
||||
existing, found := availablePluginsMap[p.Manifest.Id]
|
||||
if !found {
|
||||
logger.Info("Should persist transitionally prepackaged plugin: not currently in filestore")
|
||||
return true
|
||||
}
|
||||
|
||||
prepackagedVersion, err := semver.Parse(p.Manifest.Version)
|
||||
if err != nil {
|
||||
logger.Error("Should not persist transitionally prepackged plugin: invalid prepackaged version", mlog.Err(err))
|
||||
return false
|
||||
}
|
||||
|
||||
logger = logger.With(mlog.String("existing_version", existing.Manifest.Version))
|
||||
|
||||
existingVersion, err := semver.Parse(existing.Manifest.Version)
|
||||
if err != nil {
|
||||
// Consider this an old version and replace with the prepackaged version instead.
|
||||
logger.Warn("Should persist transitionally prepackged plugin: invalid existing version", mlog.Err(err))
|
||||
return true
|
||||
}
|
||||
|
||||
if prepackagedVersion.GT(existingVersion) {
|
||||
logger.Info("Should persist transitionally prepackged plugin: newer version")
|
||||
return true
|
||||
}
|
||||
|
||||
logger.Info("Should not persist transitionally prepackged plugin: not a newer version")
|
||||
return false
|
||||
}
|
||||
|
||||
// persistTransitionallyPrepackagedPlugins writes plugins that are transitionally prepackaged with
|
||||
// the server to the filestore to allow their continued use when the plugin eventually stops being
|
||||
// prepackaged.
|
||||
//
|
||||
// We identify which plugins need to be persisted during startup via processPrepackagedPlugins.
|
||||
// Once we persist the set of plugins to the filestore, we clear the list to prevent this server
|
||||
// from trying again.
|
||||
//
|
||||
// In a multi-server cluster, only the cluster leader should persist these plugins to avoid
|
||||
// concurrent writes to the filestore. But during an upgrade, there's no guarantee that a freshly
|
||||
// upgraded server will be the cluster leader to perform this step in a timely fashion, so the
|
||||
// persistence has to be able to happen sometime after startup. Additionally, while this is a
|
||||
// kind of migration, it's not a one off: new versions of these plugins may still be shipped
|
||||
// during the transition period, or new plugins may be added to the list.
|
||||
//
|
||||
// So instead of a one-time migration, we opt to run this method every time the cluster leader
|
||||
// changes, but minimizing rework. More than one server may end up persisting the same plugin
|
||||
// (but never concurrently!), but all servers will eventually converge on this method becoming a
|
||||
// no-op (until this set of plugins changes in a subsequent release).
|
||||
//
|
||||
// Finally, if an error occurs persisting the plugin, we don't try again until the server restarts,
|
||||
// or another server becomes cluster leader.
|
||||
func (ch *Channels) persistTransitionallyPrepackagedPlugins() {
|
||||
if !ch.srv.IsLeader() {
|
||||
ch.srv.Log().Debug("Not persisting transitionally prepackaged plugins: not the leader")
|
||||
return
|
||||
}
|
||||
|
||||
pluginsEnvironment := ch.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
ch.srv.Log().Debug("Not persisting transitionally prepackaged plugins: no plugin environment")
|
||||
return
|
||||
}
|
||||
|
||||
transitionallyPrepackagedPlugins := pluginsEnvironment.TransitionallyPrepackagedPlugins()
|
||||
if len(transitionallyPrepackagedPlugins) == 0 {
|
||||
ch.srv.Log().Debug("Not persisting transitionally prepackaged plugins: none found")
|
||||
return
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, p := range transitionallyPrepackagedPlugins {
|
||||
wg.Add(1)
|
||||
go func(p *plugin.PrepackagedPlugin) {
|
||||
defer wg.Done()
|
||||
|
||||
logger := ch.srv.Log().With(mlog.String("plugin_id", p.Manifest.Id), mlog.String("version", p.Manifest.Version))
|
||||
|
||||
logger.Info("Persisting transitionally prepackaged plugin")
|
||||
|
||||
bundleReader, err := os.Open(p.Path)
|
||||
if err != nil {
|
||||
logger.Error("Failed to read transitionally prepackaged plugin", mlog.Err(err))
|
||||
}
|
||||
defer bundleReader.Close()
|
||||
|
||||
signatureReader := bytes.NewReader(p.Signature)
|
||||
|
||||
// Write the plugin to the filestore, but don't bother notifying the peers,
|
||||
// as there's no reason to reload the plugin to run the same version again.
|
||||
appErr := ch.installPluginToFilestore(p.Manifest, bundleReader, signatureReader)
|
||||
if appErr != nil {
|
||||
logger.Error("Failed to persist transitionally prepackaged plugin", mlog.Err(err))
|
||||
}
|
||||
}(p)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
pluginsEnvironment.ClearTransitionallyPrepackagedPlugins()
|
||||
ch.srv.Log().Info("Finished persisting transitionally prepackaged plugins")
|
||||
}
|
||||
|
||||
// buildPrepackagedPlugin builds a PrepackagedPlugin from the plugin at the given path, additionally returning the directory in which it was extracted.
|
||||
func (ch *Channels) buildPrepackagedPlugin(pluginPath *pluginSignaturePath, pluginFile io.ReadSeeker, tmpDir string) (*plugin.PrepackagedPlugin, string, error) {
|
||||
manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir)
|
||||
|
||||
@@ -1,38 +1,85 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Installing a managed plugin consists of copying the uploaded plugin (*.tar.gz) to the filestore,
|
||||
// unpacking to the configured local directory (PluginSettings.Directory), and copying any webapp bundle therein
|
||||
// to the configured local client directory (PluginSettings.ClientDirectory). The unpacking and copy occurs
|
||||
// each time the server starts, ensuring it remains synchronized with the set of installed plugins.
|
||||
// ### Plugin Bundles
|
||||
//
|
||||
// When a plugin is enabled, all connected websocket clients are notified so as to fetch any webapp bundle and
|
||||
// load the client-side portion of the plugin. This works well in a single-server system, but requires careful
|
||||
// coordination in a high-availability cluster with multiple servers. In particular, websocket clients must not be
|
||||
// notified of the newly enabled plugin until all servers in the cluster have finished unpacking the plugin, otherwise
|
||||
// the webapp bundle might not yet be available. Ideally, each server would just notify its own set of connected peers
|
||||
// after it finishes this process, but nothing prevents those clients from re-connecting to a different server behind
|
||||
// the load balancer that hasn't finished unpacking.
|
||||
// A plugin bundle consists of a server and/or webapp component designed to extend the
|
||||
// functionality of the server. Bundles are first extracted to the configured local directory
|
||||
// (PluginSettings.Directory), with any webapp component additionally copied to the configured
|
||||
// local client directory (PluginSettings.ClientDirectory) to be loaded alongside the webapp.
|
||||
//
|
||||
// To achieve this coordination, each server instead checks the status of its peers after unpacking. If it finds peers with
|
||||
// differing versions of the plugin, it skips the notification. If it finds all peers with the same version of the plugin,
|
||||
// it notifies all websocket clients connected to all peers. There's a small chance that this never occurs if the last
|
||||
// server to finish unpacking dies before it can announce. There is also a chance that multiple servers decide to notify,
|
||||
// but the webapp handles this idempotently.
|
||||
// Plugin bundles are sourced in one of three ways:
|
||||
// - plugins prepackged with the server in the prepackaged_plugins/ directory
|
||||
// - plugins transitionally prepackaged with the server in the prepackaged_plugins/ directory
|
||||
// - plugins installed to the filestore (amazons3 or local, alongisde files and images)
|
||||
// - unmanaged plugins manually extracted to the confgured local directory
|
||||
// ┌────────────────────────────┐
|
||||
// │ ┌────────────────────────┐ │
|
||||
// │ │prepackaged_plugins/ │ │
|
||||
// │ │ prepackaged.tar.gz │ │
|
||||
// │ │ transitional.tar.gz │ │
|
||||
// │ │ │ │
|
||||
// │ └────────────────────────┘ │
|
||||
// │ │ │
|
||||
// │ ▼ │
|
||||
// │ ┌────────────────────────┐ │
|
||||
// │ │plugins/ │ │
|
||||
// │ │ unmanaged/ │ │
|
||||
// │ │ filestore/ │ │ ┌────────────────────────┐
|
||||
// │ │ .filestore │ │ │s3://bucket/plugins/ │
|
||||
// │ │ prepackaged/ │◀┼───│ filestore.tar.gz │
|
||||
// │ │ .filestore │ │ │ transitional.tar.gz │
|
||||
// │ │ transitional/ │ │ └────────────────────────┘
|
||||
// │ │ .filestore │ │
|
||||
// │ └────────────────────────┘ │
|
||||
// │ ┌────────┤
|
||||
// │ │ server │
|
||||
// └───────────────────┴────────┘
|
||||
//
|
||||
// Complicating this flow further are the various means of notifying. In addition to websocket events, there are cluster
|
||||
// messages between peers. There is a cluster message when the config changes and a plugin is enabled or disabled.
|
||||
// There is a cluster message when installing or uninstalling a plugin. There is a cluster message when peer's plugin change
|
||||
// its status. And finally the act of notifying websocket clients is propagated itself via a cluster message.
|
||||
// Prepackaged plugins are bundles shipped alongside the server to simplify installation and
|
||||
// upgrade. This occurs automatically if configured (PluginSettings.AutomaticPrepackagedPlugins)
|
||||
// and the plugin is enabled (PluginSettings.PluginStates[plugin_id].Enable), unless a matching or
|
||||
// newer version of the plugin is already installed.
|
||||
//
|
||||
// The key methods involved in handling these notifications are notifyPluginEnabled and notifyPluginStatusesChanged.
|
||||
// Note that none of this complexity applies to single-server systems or to plugins without a webapp bundle.
|
||||
// Transitionally prepackaged plugins are bundles that will stop being prepackaged in a future
|
||||
// release. On first startup, they are unpacked just like prepackaged plugins, but also get copied
|
||||
// to the filestore. On future startups, the server uses the version in the filestore.
|
||||
//
|
||||
// Finally, in addition to managed plugins, note that there are unmanaged and prepackaged plugins.
|
||||
// Unmanaged plugins are plugins installed manually to the configured local directory (PluginSettings.Directory).
|
||||
// Prepackaged plugins are included with the server. They otherwise follow the above flow, except do not get uploaded
|
||||
// to the filestore. Prepackaged plugins override all other plugins with the same plugin id, but only when the prepackaged
|
||||
// plugin is newer. Managed plugins unconditionally override unmanaged plugins with the same plugin id.
|
||||
// Plugins are installed to the filestore when the user installs via the marketplace or manually
|
||||
// uploads a plugin bundle. (Or because the plugin is transitionally prepackaged).
|
||||
//
|
||||
// Unmanaged plugins were manually extracted by into the configured local directory. This legacy
|
||||
// method of installing plugins is distinguished from other extracted plugins by the absence of a
|
||||
// flag file (.filestore). Managed plugins unconditionally override unmanaged plugins. A future
|
||||
// version of Mattermost will likely drop support for unmanaged plugins.
|
||||
//
|
||||
// ### Enabling a Plugin
|
||||
//
|
||||
// When a plugin is enabled, all connected websocket clients are notified so as to fetch any
|
||||
// webapp bundle and load the client-side portion of the plugin. This works well in a
|
||||
// single-server system, but requires careful coordination in a high-availability cluster with
|
||||
// multiple servers. In particular, websocket clients must not be notified of the newly enabled
|
||||
// plugin until all servers in the cluster have finished unpacking the plugin, otherwise the
|
||||
// webapp bundle might not yet be available. Ideally, each server would just notify its own set of
|
||||
// connected peers after it finishes this process, but nothing prevents those clients from
|
||||
// re-connecting to a different server behind the load balancer that hasn't finished unpacking.
|
||||
//
|
||||
// To achieve this coordination, each server instead checks the status of its peers after
|
||||
// unpacking. If it finds peers with differing versions of the plugin, it skips the notification.
|
||||
// If it finds all peers with the same version of the plugin, it notifies all websocket clients
|
||||
// connected to all peers. There's a small chance that this never occurs if the last server to
|
||||
// finish unpacking dies before it can announce. There is also a chance that multiple servers
|
||||
// decide to notify, but the webapp handles this idempotently.
|
||||
//
|
||||
// Complicating this flow further are the various means of notifying. In addition to websocket
|
||||
// events, there are cluster messages between peers. There is a cluster message when the config
|
||||
// changes and a plugin is enabled or disabled. There is a cluster message when installing or
|
||||
// uninstalling a plugin. There is a cluster message when a peer's plugin changes its status. And
|
||||
// finally the act of notifying websocket clients is itself propagated via a cluster message.
|
||||
//
|
||||
// The key methods involved in handling these notifications are notifyPluginEnabled and
|
||||
// notifyPluginStatusesChanged. Note that none of this complexity applies to single-server
|
||||
// systems or to plugins without a webapp bundle.
|
||||
package app
|
||||
|
||||
import (
|
||||
@@ -91,9 +138,14 @@ func (ch *Channels) installPluginFromClusterMessage(pluginID string) {
|
||||
return
|
||||
}
|
||||
defer signature.Close()
|
||||
|
||||
if err := ch.verifyPlugin(bundle, signature); err != nil {
|
||||
mlog.Error("Failed to validate plugin signature.", mlog.Err(appErr))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
manifest, appErr := ch.installPluginLocally(bundle, signature, installPluginLocallyAlways)
|
||||
manifest, appErr := ch.installPluginLocally(bundle, installPluginLocallyAlways)
|
||||
if appErr != nil {
|
||||
// A log line already appears if the plugin is on the blocklist or skipped
|
||||
if appErr.Id != "app.plugin.blocked.app_error" && appErr.Id != "app.plugin.skip_installation.app_error" {
|
||||
@@ -144,7 +196,7 @@ func (a *App) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Mani
|
||||
//
|
||||
// The given installation strategy decides how to handle upgrade scenarios.
|
||||
func (ch *Channels) installPlugin(bundle, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
|
||||
manifest, appErr := ch.installPluginLocally(bundle, signature, installationStrategy)
|
||||
manifest, appErr := ch.installPluginLocally(bundle, installationStrategy)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
@@ -161,7 +213,7 @@ func (ch *Channels) installPlugin(bundle, signature io.ReadSeeker, installationS
|
||||
}
|
||||
|
||||
if err := ch.notifyPluginEnabled(manifest); err != nil {
|
||||
logger.Warn("Failed notify plugin enabled", mlog.Err(err))
|
||||
logger.Warn("Failed to notify plugin enabled", mlog.Err(err))
|
||||
}
|
||||
|
||||
if err := ch.notifyPluginStatusesChanged(); err != nil {
|
||||
@@ -174,20 +226,33 @@ func (ch *Channels) installPlugin(bundle, signature io.ReadSeeker, installationS
|
||||
// installPluginToFilestore saves the given plugin bundle (optionally signed) to the filestore,
|
||||
// notifying cluster peers accordingly.
|
||||
func (ch *Channels) installPluginToFilestore(manifest *model.Manifest, bundle, signature io.ReadSeeker) *model.AppError {
|
||||
if signature != nil {
|
||||
logger := ch.srv.Log().With(mlog.String("plugin_id", manifest.Id))
|
||||
logger.Info("Persisting plugin to filestore")
|
||||
|
||||
if signature == nil {
|
||||
logger.Warn("No signature when persisting plugin to filestore")
|
||||
} else {
|
||||
signatureStorePath := getSignatureStorePath(manifest.Id)
|
||||
_, err := signature.Seek(0, 0)
|
||||
if err != nil {
|
||||
return model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
if _, appErr := ch.srv.writeFile(signature, getSignatureStorePath(manifest.Id)); appErr != nil {
|
||||
logger.Debug("Persisting plugin signature to filestore", mlog.String("path", signatureStorePath))
|
||||
if _, appErr := ch.srv.writeFile(signature, signatureStorePath); appErr != nil {
|
||||
return model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Store bundle in the file store to allow access from other servers.
|
||||
bundle.Seek(0, 0)
|
||||
if _, appErr := ch.srv.writeFile(bundle, getBundleStorePath(manifest.Id)); appErr != nil {
|
||||
bundleStorePath := getBundleStorePath(manifest.Id)
|
||||
_, err := bundle.Seek(0, 0)
|
||||
if err != nil {
|
||||
return model.NewAppError("uploadPlugin", "app.plugin.store_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
logger.Debug("Persisting plugin bundle to filestore", mlog.String("path", bundleStorePath))
|
||||
if _, appErr := ch.srv.writeFile(bundle, bundleStorePath); appErr != nil {
|
||||
return model.NewAppError("uploadPlugin", "app.plugin.store_bundle.app_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
|
||||
}
|
||||
|
||||
@@ -269,6 +334,11 @@ func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePl
|
||||
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.signature_not_found.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
appErr = ch.verifyPlugin(pluginFile, signatureFile)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
manifest, appErr := ch.installPlugin(pluginFile, signatureFile, installPluginLocallyAlways)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
@@ -288,23 +358,16 @@ const (
|
||||
installPluginLocallyAlways
|
||||
)
|
||||
|
||||
// installPluginLocally extracts and installs the given plugin bundle (optionally signed) for the
|
||||
// current server, activating the plugin if already enabled.
|
||||
// installPluginLocally extracts and installs the given plugin bundle for the current server,
|
||||
// activating the plugin if already enabled.
|
||||
//
|
||||
// The given installation strategy decides how to handle upgrade scenarios.
|
||||
func (ch *Channels) installPluginLocally(bundle, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
|
||||
func (ch *Channels) installPluginLocally(bundle io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
|
||||
pluginsEnvironment := ch.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// verify signature
|
||||
if signature != nil {
|
||||
if err := ch.verifyPlugin(bundle, signature); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "plugintmp")
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.filesystem.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestInstallPluginLocally(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
actualManifest, appErr := th.App.ch.installPluginLocally(&nilReadSeeker{}, nil, installPluginLocallyOnlyIfNew)
|
||||
actualManifest, appErr := th.App.ch.installPluginLocally(&nilReadSeeker{}, installPluginLocallyOnlyIfNew)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.plugin.extract.app_error", appErr.Id, appErr.Error())
|
||||
require.Nil(t, actualManifest)
|
||||
@@ -87,7 +87,7 @@ func TestInstallPluginLocally(t *testing.T) {
|
||||
{"test", "test file"},
|
||||
})
|
||||
|
||||
actualManifest, appErr := th.App.ch.installPluginLocally(reader, nil, installPluginLocallyOnlyIfNew)
|
||||
actualManifest, appErr := th.App.ch.installPluginLocally(reader, installPluginLocallyOnlyIfNew)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.plugin.manifest.app_error", appErr.Id, appErr.Error())
|
||||
require.Nil(t, actualManifest)
|
||||
@@ -106,7 +106,7 @@ func TestInstallPluginLocally(t *testing.T) {
|
||||
{"plugin.json", string(manifestJSON)},
|
||||
})
|
||||
|
||||
actualManifest, appError := th.App.ch.installPluginLocally(reader, nil, installationStrategy)
|
||||
actualManifest, appError := th.App.ch.installPluginLocally(reader, installationStrategy)
|
||||
if actualManifest != nil {
|
||||
require.Equal(t, manifest, actualManifest)
|
||||
}
|
||||
|
||||
@@ -4,15 +4,19 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -805,202 +809,566 @@ func TestPluginStatusActivateError(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
type byId []*plugin.PrepackagedPlugin
|
||||
|
||||
func (a byId) Len() int { return len(a) }
|
||||
func (a byId) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byId) Less(i, j int) bool { return a[i].Manifest.Id < a[j].Manifest.Id }
|
||||
|
||||
type pluginStatusById model.PluginStatuses
|
||||
|
||||
func (a pluginStatusById) Len() int { return len(a) }
|
||||
func (a pluginStatusById) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a pluginStatusById) Less(i, j int) bool { return a[i].PluginId < a[j].PluginId }
|
||||
|
||||
func TestProcessPrepackagedPlugins(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
// Find the tests folder before we change directories to the temporary workspace.
|
||||
testsPath, _ := fileutils.FindDir("tests")
|
||||
prepackagedPluginsPath := filepath.Join(testsPath, prepackagedPluginsDir)
|
||||
fileErr := os.Mkdir(prepackagedPluginsPath, os.ModePerm)
|
||||
require.NoError(t, fileErr)
|
||||
defer os.RemoveAll(prepackagedPluginsPath)
|
||||
|
||||
prepackagedPluginsDir, found := fileutils.FindDir(prepackagedPluginsPath)
|
||||
require.True(t, found, "failed to find prepackaged plugins directory")
|
||||
setup := func(t *testing.T) *TestHelper {
|
||||
t.Helper()
|
||||
|
||||
testPluginPath := filepath.Join(testsPath, "testplugin.tar.gz")
|
||||
fileErr = testlib.CopyFile(testPluginPath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz"))
|
||||
require.NoError(t, fileErr)
|
||||
th := Setup(t)
|
||||
t.Cleanup(th.TearDown)
|
||||
|
||||
t.Run("automatic, enabled plugin, no signature", func(t *testing.T) {
|
||||
// Install the plugin and enable
|
||||
pluginBytes, err := os.ReadFile(testPluginPath)
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pluginBytes)
|
||||
t.Cleanup(func() {
|
||||
err = os.Chdir(wd)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
manifest, appErr := th.App.ch.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, "testplugin", manifest.Id)
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
activatedManifest, activated, err := env.Activate(manifest.Id)
|
||||
err = os.Chdir(th.tempWorkspace)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Make a prepackaged_plugins directory for use with the tests.
|
||||
err = os.Mkdir(prepackagedPluginsDir, os.ModePerm)
|
||||
require.NoError(t, err)
|
||||
require.True(t, activated)
|
||||
require.Equal(t, manifest, activatedManifest)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = false
|
||||
})
|
||||
|
||||
plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, plugins[0].Manifest.Id, "testplugin")
|
||||
require.Empty(t, plugins[0].Signature, 0)
|
||||
return th
|
||||
}
|
||||
|
||||
initPlugins := func(t *testing.T, th *TestHelper) ([]*plugin.PrepackagedPlugin, []*plugin.PrepackagedPlugin) {
|
||||
t.Helper()
|
||||
|
||||
appErr := th.App.ch.syncPlugins()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
err := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
plugins, transitionalPlugins := env.PrepackagedPlugins(), env.TransitionallyPrepackagedPlugins()
|
||||
th.App.ch.persistTransitionallyPrepackagedPlugins()
|
||||
|
||||
return plugins, transitionalPlugins
|
||||
}
|
||||
|
||||
copyAsPrepackagedPlugin := func(t *testing.T, filename string) {
|
||||
t.Helper()
|
||||
|
||||
err := testlib.CopyFile(filepath.Join(testsPath, filename), filepath.Join(prepackagedPluginsDir, filename))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = testlib.CopyFile(filepath.Join(testsPath, fmt.Sprintf("%s.sig", filename)), filepath.Join(prepackagedPluginsDir, fmt.Sprintf("%s.sig", filename)))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
copyAsFilestorePlugin := func(t *testing.T, bundleFilename, pluginID string) {
|
||||
t.Helper()
|
||||
|
||||
err := testlib.CopyFile(filepath.Join(testsPath, bundleFilename), filepath.Join(fmt.Sprintf("data/plugins/%s.tar.gz", pluginID)))
|
||||
require.NoError(t, err)
|
||||
err = testlib.CopyFile(filepath.Join(testsPath, fmt.Sprintf("%s.sig", bundleFilename)), filepath.Join(fmt.Sprintf("data/plugins/%s.tar.gz.sig", pluginID)))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
expectPrepackagedPlugin := func(t *testing.T, pluginID, version string, actual *plugin.PrepackagedPlugin) {
|
||||
t.Helper()
|
||||
|
||||
require.Equal(t, pluginID, actual.Manifest.Id)
|
||||
require.NotEmpty(t, actual.Signature, "testplugin has no signature")
|
||||
require.Equal(t, version, actual.Manifest.Version)
|
||||
}
|
||||
|
||||
expectPluginInFilestore := func(t *testing.T, pluginID, version string) {
|
||||
t.Helper()
|
||||
|
||||
bundlePath := fmt.Sprintf("data/plugins/%s.tar.gz", pluginID)
|
||||
|
||||
require.FileExists(t, bundlePath)
|
||||
require.FileExists(t, fmt.Sprintf("data/plugins/%s.tar.gz.sig", pluginID))
|
||||
|
||||
// Verify the version recorded in the Manifest
|
||||
f, err := os.Open(bundlePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
uncompressedStream, err := gzip.NewReader(f)
|
||||
require.NoError(t, err)
|
||||
|
||||
tarReader := tar.NewReader(uncompressedStream)
|
||||
|
||||
var manifest model.Manifest
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log(header.Name)
|
||||
if header.Typeflag == tar.TypeReg && filepath.Base(header.Name) == "plugin.json" {
|
||||
manifestReader := json.NewDecoder(tarReader)
|
||||
err = manifestReader.Decode(&manifest)
|
||||
require.NoError(t, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, pluginID, manifest.Id, "failed to find manifest")
|
||||
require.Equal(t, version, manifest.Version)
|
||||
}
|
||||
|
||||
expectPluginNotInFilestore := func(t *testing.T, pluginID string) {
|
||||
t.Helper()
|
||||
|
||||
require.NoFileExists(t, fmt.Sprintf("data/plugins/%s.tar.gz", pluginID))
|
||||
require.NoFileExists(t, fmt.Sprintf("data/plugins/%s.tar.gz.sig", pluginID))
|
||||
}
|
||||
|
||||
expectPluginStatus := func(t *testing.T, pluginID, version string, actual *model.PluginStatus) {
|
||||
t.Helper()
|
||||
|
||||
require.Equal(t, pluginID, actual.PluginId)
|
||||
require.Equal(t, version, actual.Version)
|
||||
}
|
||||
|
||||
t.Run("single plugin automatically installed since enabled", func(t *testing.T) {
|
||||
th := setup(t)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
copyAsPrepackagedPlugin(t, "testplugin.tar.gz")
|
||||
|
||||
plugins, transitionalPlugins := initPlugins(t, th)
|
||||
require.Len(t, plugins, 1, "expected one prepackaged plugin")
|
||||
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
||||
require.Empty(t, transitionalPlugins)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
expectPluginStatus(t, "testplugin", "0.0.1", pluginStatus[0])
|
||||
|
||||
appErr = th.App.ch.RemovePlugin("testplugin")
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
expectPluginNotInFilestore(t, "testplugin")
|
||||
})
|
||||
|
||||
t.Run("automatic, not enabled plugin", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = false
|
||||
})
|
||||
|
||||
t.Run("single plugin, not automatically installed since not enabled", func(t *testing.T) {
|
||||
th := setup(t)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, plugins[0].Manifest.Id, "testplugin")
|
||||
require.Empty(t, plugins[0].Signature, 0)
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
})
|
||||
|
||||
copyAsPrepackagedPlugin(t, "testplugin.tar.gz")
|
||||
|
||||
plugins, transitionalPlugins := initPlugins(t, th)
|
||||
require.Len(t, plugins, 1, "expected one prepackaged plugin")
|
||||
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
||||
require.Empty(t, transitionalPlugins)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, pluginStatus, 0)
|
||||
|
||||
expectPluginNotInFilestore(t, "testplugin")
|
||||
})
|
||||
|
||||
t.Run("automatic, multiple plugins with signatures, not enabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = false
|
||||
})
|
||||
|
||||
t.Run("single plugin, not automatically installed despite enabled since automatic prepackaged plugins disabled", func(t *testing.T) {
|
||||
th := setup(t)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
// Add signature
|
||||
testPluginSignaturePath := filepath.Join(testsPath, "testplugin.tar.gz.sig")
|
||||
err := testlib.CopyFile(testPluginSignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
// Add second plugin
|
||||
testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz")
|
||||
err = testlib.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
copyAsPrepackagedPlugin(t, "testplugin.tar.gz")
|
||||
|
||||
testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig")
|
||||
err = testlib.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
|
||||
plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 2)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[0].Signature)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[1].Signature)
|
||||
plugins, transitionalPlugins := initPlugins(t, th)
|
||||
require.Len(t, plugins, 1, "expected one prepackaged plugin")
|
||||
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
||||
require.Empty(t, transitionalPlugins)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
require.Empty(t, pluginStatus, 0)
|
||||
|
||||
expectPluginNotInFilestore(t, "testplugin")
|
||||
})
|
||||
|
||||
t.Run("automatic, multiple plugins with signatures, one enabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = false
|
||||
})
|
||||
|
||||
t.Run("multiple plugins, some automatically installed since enabled", func(t *testing.T) {
|
||||
th := setup(t)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
// Add signature
|
||||
testPluginSignaturePath := filepath.Join(testsPath, "testplugin.tar.gz.sig")
|
||||
err := testlib.CopyFile(testPluginSignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
// Install first plugin and enable
|
||||
pluginBytes, err := os.ReadFile(testPluginPath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pluginBytes)
|
||||
copyAsPrepackagedPlugin(t, "testplugin.tar.gz")
|
||||
copyAsPrepackagedPlugin(t, "testplugin2.tar.gz")
|
||||
|
||||
manifest, appErr := th.App.ch.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, "testplugin", manifest.Id)
|
||||
|
||||
activatedManifest, activated, err := env.Activate(manifest.Id)
|
||||
require.NoError(t, err)
|
||||
require.True(t, activated)
|
||||
require.Equal(t, manifest, activatedManifest)
|
||||
|
||||
// Add second plugin
|
||||
testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz")
|
||||
err = testlib.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
|
||||
testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig")
|
||||
err = testlib.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
|
||||
plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 2)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[0].Signature)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[1].Signature)
|
||||
plugins, transitionalPlugins := initPlugins(t, th)
|
||||
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
||||
sort.Sort(byId(plugins))
|
||||
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
||||
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
||||
require.Empty(t, transitionalPlugins)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[0])
|
||||
|
||||
appErr = th.App.ch.RemovePlugin("testplugin")
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
expectPluginNotInFilestore(t, "testplugin")
|
||||
expectPluginNotInFilestore(t, "testplugin2")
|
||||
})
|
||||
|
||||
t.Run("non-automatic, multiple plugins", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = false
|
||||
})
|
||||
|
||||
t.Run("multiple plugins, one previously installed, all now installed", func(t *testing.T) {
|
||||
th := setup(t)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz")
|
||||
err := testlib.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz"))
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
})
|
||||
|
||||
copyAsFilestorePlugin(t, "testplugin.tar.gz", "testplugin")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
||||
cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
copyAsPrepackagedPlugin(t, "testplugin.tar.gz")
|
||||
copyAsPrepackagedPlugin(t, "testplugin2.tar.gz")
|
||||
|
||||
plugins, transitionalPlugins := initPlugins(t, th)
|
||||
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
||||
sort.Sort(byId(plugins))
|
||||
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
||||
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
||||
require.Empty(t, transitionalPlugins)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
|
||||
testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig")
|
||||
err = testlib.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig"))
|
||||
sort.Sort(pluginStatusById(pluginStatus))
|
||||
|
||||
require.Len(t, pluginStatus, 2)
|
||||
expectPluginStatus(t, "testplugin", "0.0.1", pluginStatus[0])
|
||||
expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[1])
|
||||
|
||||
expectPluginInFilestore(t, "testplugin", "0.0.1")
|
||||
expectPluginNotInFilestore(t, "testplugin2")
|
||||
})
|
||||
|
||||
t.Run("multiple plugins, one previously installed and now upgraded, all now installed", func(t *testing.T) {
|
||||
th := setup(t)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
})
|
||||
|
||||
copyAsFilestorePlugin(t, "testplugin.tar.gz", "testplugin")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
||||
cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
copyAsPrepackagedPlugin(t, "testplugin-v0.0.2.tar.gz")
|
||||
copyAsPrepackagedPlugin(t, "testplugin2.tar.gz")
|
||||
|
||||
plugins, transitionalPlugins := initPlugins(t, th)
|
||||
err := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
plugins := th.App.ch.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 2)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[0].Signature)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[1].Signature)
|
||||
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
||||
sort.Sort(byId(plugins))
|
||||
expectPrepackagedPlugin(t, "testplugin", "0.0.2", plugins[0])
|
||||
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
||||
require.Empty(t, transitionalPlugins)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Sort(pluginStatusById(pluginStatus))
|
||||
|
||||
require.Len(t, pluginStatus, 2)
|
||||
expectPluginStatus(t, "testplugin", "0.0.2", pluginStatus[0])
|
||||
expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[1])
|
||||
|
||||
expectPluginInFilestore(t, "testplugin", "0.0.1")
|
||||
expectPluginNotInFilestore(t, "testplugin2")
|
||||
})
|
||||
|
||||
t.Run("multiple plugins, one previously installed but prepackaged is older, all now installed", func(t *testing.T) {
|
||||
th := setup(t)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
})
|
||||
|
||||
copyAsFilestorePlugin(t, "testplugin-v0.0.2.tar.gz", "testplugin")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
||||
cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
copyAsPrepackagedPlugin(t, "testplugin.tar.gz")
|
||||
copyAsPrepackagedPlugin(t, "testplugin2.tar.gz")
|
||||
|
||||
plugins, transitionalPlugins := initPlugins(t, th)
|
||||
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
||||
sort.Sort(byId(plugins))
|
||||
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
||||
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
||||
require.Empty(t, transitionalPlugins)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Sort(pluginStatusById(pluginStatus))
|
||||
|
||||
require.Len(t, pluginStatus, 2)
|
||||
expectPluginStatus(t, "testplugin", "0.0.2", pluginStatus[0])
|
||||
expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[1])
|
||||
|
||||
expectPluginInFilestore(t, "testplugin", "0.0.2")
|
||||
expectPluginNotInFilestore(t, "testplugin2")
|
||||
})
|
||||
|
||||
t.Run("multiple plugins, not automatically installed despite enabled since automatic prepackaged plugins disabled", func(t *testing.T) {
|
||||
th := setup(t)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
||||
cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
copyAsPrepackagedPlugin(t, "testplugin.tar.gz")
|
||||
copyAsPrepackagedPlugin(t, "testplugin2.tar.gz")
|
||||
|
||||
plugins, transitionalPlugins := initPlugins(t, th)
|
||||
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
||||
sort.Sort(byId(plugins))
|
||||
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
||||
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
||||
require.Empty(t, transitionalPlugins)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
|
||||
expectPluginNotInFilestore(t, "testplugin")
|
||||
expectPluginNotInFilestore(t, "testplugin2")
|
||||
})
|
||||
|
||||
t.Run("removing a prepackaged plugin leaves it disabled", func(t *testing.T) {
|
||||
th := setup(t)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
||||
cfg.PluginSettings.PluginStates["testplugin2"] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
copyAsPrepackagedPlugin(t, "testplugin.tar.gz")
|
||||
copyAsPrepackagedPlugin(t, "testplugin2.tar.gz")
|
||||
|
||||
plugins, transitionalPlugins := initPlugins(t, th)
|
||||
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
||||
sort.Sort(byId(plugins))
|
||||
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
||||
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
||||
require.Empty(t, transitionalPlugins)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
|
||||
sort.Sort(pluginStatusById(pluginStatus))
|
||||
|
||||
require.Len(t, pluginStatus, 2)
|
||||
require.Equal(t, "testplugin", pluginStatus[0].PluginId)
|
||||
require.Equal(t, "testplugin2", pluginStatus[1].PluginId)
|
||||
|
||||
appErr := th.App.ch.RemovePlugin("testplugin")
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, "testplugin2", pluginStatus[0].PluginId)
|
||||
|
||||
plugins, transitionalPlugins = initPlugins(t, th)
|
||||
require.Len(t, plugins, 2, "expected two prepackaged plugins")
|
||||
sort.Sort(byId(plugins))
|
||||
expectPrepackagedPlugin(t, "testplugin", "0.0.1", plugins[0])
|
||||
expectPrepackagedPlugin(t, "testplugin2", "1.2.3", plugins[1])
|
||||
require.Empty(t, transitionalPlugins)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
expectPluginStatus(t, "testplugin2", "1.2.3", pluginStatus[0])
|
||||
|
||||
expectPluginNotInFilestore(t, "testplugin")
|
||||
expectPluginNotInFilestore(t, "testplugin2")
|
||||
})
|
||||
|
||||
t.Run("single transitional plugin automatically installed and persisted since enabled", func(t *testing.T) {
|
||||
th := setup(t)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
oldTransitionallyPrepackagedPlugins := append([]string{}, transitionallyPrepackagedPlugins...)
|
||||
transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, "testplugin")
|
||||
defer func() {
|
||||
transitionallyPrepackagedPlugins = oldTransitionallyPrepackagedPlugins
|
||||
}()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
copyAsPrepackagedPlugin(t, "testplugin.tar.gz")
|
||||
|
||||
plugins, transitionalPlugins := initPlugins(t, th)
|
||||
require.Empty(t, plugins)
|
||||
require.Len(t, transitionalPlugins, 1)
|
||||
expectPrepackagedPlugin(t, "testplugin", "0.0.1", transitionalPlugins[0])
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
expectPluginStatus(t, "testplugin", "0.0.1", pluginStatus[0])
|
||||
|
||||
expectPluginInFilestore(t, "testplugin", "0.0.1")
|
||||
expectPluginNotInFilestore(t, "testplugin2")
|
||||
})
|
||||
|
||||
t.Run("single transitional plugin not persisted since already in filestore", func(t *testing.T) {
|
||||
th := setup(t)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
oldTransitionallyPrepackagedPlugins := append([]string{}, transitionallyPrepackagedPlugins...)
|
||||
transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, "testplugin")
|
||||
defer func() {
|
||||
transitionallyPrepackagedPlugins = oldTransitionallyPrepackagedPlugins
|
||||
}()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
copyAsFilestorePlugin(t, "testplugin.tar.gz", "testplugin")
|
||||
copyAsPrepackagedPlugin(t, "testplugin.tar.gz")
|
||||
|
||||
plugins, transitionalPlugins := initPlugins(t, th)
|
||||
require.Empty(t, plugins)
|
||||
require.Empty(t, transitionalPlugins)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
expectPluginStatus(t, "testplugin", "0.0.1", pluginStatus[0])
|
||||
|
||||
expectPluginInFilestore(t, "testplugin", "0.0.1")
|
||||
expectPluginNotInFilestore(t, "testplugin2")
|
||||
})
|
||||
|
||||
t.Run("single transitional plugin persisted since newer than filestore", func(t *testing.T) {
|
||||
th := setup(t)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
oldTransitionallyPrepackagedPlugins := append([]string{}, transitionallyPrepackagedPlugins...)
|
||||
transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, "testplugin")
|
||||
defer func() {
|
||||
transitionallyPrepackagedPlugins = oldTransitionallyPrepackagedPlugins
|
||||
}()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
copyAsFilestorePlugin(t, "testplugin.tar.gz", "testplugin")
|
||||
copyAsPrepackagedPlugin(t, "testplugin-v0.0.2.tar.gz")
|
||||
|
||||
plugins, transitionalPlugins := initPlugins(t, th)
|
||||
require.Empty(t, plugins)
|
||||
require.Len(t, transitionalPlugins, 1)
|
||||
expectPrepackagedPlugin(t, "testplugin", "0.0.2", transitionalPlugins[0])
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
expectPluginStatus(t, "testplugin", "0.0.2", pluginStatus[0])
|
||||
|
||||
expectPluginInFilestore(t, "testplugin", "0.0.2")
|
||||
expectPluginNotInFilestore(t, "testplugin2")
|
||||
})
|
||||
|
||||
t.Run("transitional plugins persisted only once", func(t *testing.T) {
|
||||
th := setup(t)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
oldTransitionallyPrepackagedPlugins := append([]string{}, transitionallyPrepackagedPlugins...)
|
||||
transitionallyPrepackagedPlugins = append(transitionallyPrepackagedPlugins, "testplugin")
|
||||
defer func() {
|
||||
transitionallyPrepackagedPlugins = oldTransitionallyPrepackagedPlugins
|
||||
}()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
cfg.PluginSettings.PluginStates["testplugin"] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
copyAsPrepackagedPlugin(t, "testplugin.tar.gz")
|
||||
|
||||
plugins, transitionalPlugins := initPlugins(t, th)
|
||||
require.Empty(t, plugins)
|
||||
require.Len(t, transitionalPlugins, 1)
|
||||
expectPrepackagedPlugin(t, "testplugin", "0.0.1", transitionalPlugins[0])
|
||||
|
||||
th.App.ch.RemovePlugin("testplugin")
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, pluginStatus, 0)
|
||||
|
||||
th.App.ch.persistTransitionallyPrepackagedPlugins()
|
||||
|
||||
expectPluginNotInFilestore(t, "testplugin")
|
||||
expectPluginNotInFilestore(t, "testplugin2")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -48,16 +48,17 @@ type PrepackagedPlugin struct {
|
||||
// It is meant for use by the Mattermost server to manipulate, interact with and report on the set
|
||||
// of active plugins.
|
||||
type Environment struct {
|
||||
registeredPlugins sync.Map
|
||||
pluginHealthCheckJob *PluginHealthCheckJob
|
||||
logger *mlog.Logger
|
||||
metrics metricsInterface
|
||||
newAPIImpl apiImplCreatorFunc
|
||||
dbDriver Driver
|
||||
pluginDir string
|
||||
webappPluginDir string
|
||||
prepackagedPlugins []*PrepackagedPlugin
|
||||
prepackagedPluginsLock sync.RWMutex
|
||||
registeredPlugins sync.Map
|
||||
pluginHealthCheckJob *PluginHealthCheckJob
|
||||
logger *mlog.Logger
|
||||
metrics metricsInterface
|
||||
newAPIImpl apiImplCreatorFunc
|
||||
dbDriver Driver
|
||||
pluginDir string
|
||||
webappPluginDir string
|
||||
prepackagedPlugins []*PrepackagedPlugin
|
||||
transitionallyPrepackagedPlugins []*PrepackagedPlugin
|
||||
prepackagedPluginsLock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewEnvironment(
|
||||
@@ -108,7 +109,9 @@ func (env *Environment) Available() ([]*model.BundleInfo, error) {
|
||||
return scanSearchPath(env.pluginDir)
|
||||
}
|
||||
|
||||
// Returns a list of prepackaged plugins available in the local prepackaged_plugins folder.
|
||||
// Returns a list of prepackaged plugins available in the local prepackaged_plugins folder,
|
||||
// excluding those in transition out of being prepackaged.
|
||||
//
|
||||
// The list content is immutable and should not be modified.
|
||||
func (env *Environment) PrepackagedPlugins() []*PrepackagedPlugin {
|
||||
env.prepackagedPluginsLock.RLock()
|
||||
@@ -117,6 +120,26 @@ func (env *Environment) PrepackagedPlugins() []*PrepackagedPlugin {
|
||||
return env.prepackagedPlugins
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -532,9 +555,10 @@ func (env *Environment) PerformHealthCheck(id string) error {
|
||||
}
|
||||
|
||||
// SetPrepackagedPlugins saves prepackaged plugins in the environment.
|
||||
func (env *Environment) SetPrepackagedPlugins(plugins []*PrepackagedPlugin) {
|
||||
func (env *Environment) SetPrepackagedPlugins(plugins, transitionalPlugins []*PrepackagedPlugin) {
|
||||
env.prepackagedPluginsLock.Lock()
|
||||
env.prepackagedPlugins = plugins
|
||||
env.transitionallyPrepackagedPlugins = transitionalPlugins
|
||||
env.prepackagedPluginsLock.Unlock()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ It is possible to manually test specific sections of any test, instead of using
|
||||
|
||||
## Test plugins
|
||||
|
||||
There are two test plugins: `testplugin.tar.gz` and `testplugin2.tar.gz`. These are use in some integration tests in the `api4` package. Any changes to the plugin bundles require updating the corresponding signatures.
|
||||
There are three test plugins: `testplugin.tar.gz`, `testplugin-v0.0.2.tar.gz`, and `testplugin2.tar.gz`. These are use in some integration tests in the `api4` package. Any changes to the plugin bundles require updating the corresponding signatures.
|
||||
|
||||
First, import the public and private development key:
|
||||
```sh
|
||||
@@ -33,6 +33,8 @@ Then update the signatures:
|
||||
```sh
|
||||
gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign testplugin.tar.gz
|
||||
gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign --armor testplugin.tar.gz
|
||||
gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign testplugin-v0.0.2.tar.gz
|
||||
gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign --armor testplugin-v0.0.2.tar.gz
|
||||
gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign testplugin2.tar.gz
|
||||
gpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign --armor testplugin2.tar.gz
|
||||
```
|
||||
|
||||
BIN
server/tests/testplugin-v0.0.2.tar.gz
Normal file
BIN
server/tests/testplugin-v0.0.2.tar.gz
Normal file
Binary file not shown.
14
server/tests/testplugin-v0.0.2.tar.gz.asc
Normal file
14
server/tests/testplugin-v0.0.2.tar.gz.asc
Normal file
@@ -0,0 +1,14 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAmTVE0YACgkQTHxlYsGS
|
||||
zB/eiAv/V5PWAJesWktjFIWWviuiwnL3orGrlPVQ4c5d3pHslyTr7uyzAvyOhH9w
|
||||
Eg7BZC3vObV61d2XAseU0+hCUjZ7JHjRwyhTxdVj0fsuaDgl1mBbHTtZXPCKL59P
|
||||
RfXf8urkzzlSVYnMgeH158KTBtVyDaNENrhytIkNaxVy3k50tga6Z4GgN+wTfyQw
|
||||
FdLIT2piYb9UZZQT8mtw8l/Z/kkV0E038GK3PI1lLlhPHrvIo9kYG/11R061kXgg
|
||||
GxiuNEHdSftI/ZHF8f/QoPQosT5nZavhLsGE7eD+YBhDiL61cArcenGPKEsQSIIr
|
||||
K+9su+u/7XMqFX4GgsMs7uvxWz4o1ihK9l4wQ8aLDcYolUHqzplwOcQHojGutmiF
|
||||
XuqL17s7gxYTxRMHDmaePuI9mnti7cVFrSXLJwsy4qt7nAKcT2IP/qR8VBLE3R3f
|
||||
Gm15AaivKON24Oj7URz4hQfQXbNj0Ve4Pmiq85hT0VXZZAVCpuxww0J7Q0EXvaBg
|
||||
9dzylz8J
|
||||
=L0I2
|
||||
-----END PGP SIGNATURE-----
|
||||
BIN
server/tests/testplugin-v0.0.2.tar.gz.sig
Normal file
BIN
server/tests/testplugin-v0.0.2.tar.gz.sig
Normal file
Binary file not shown.
@@ -1,14 +1,14 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAmOaRXoACgkQTHxlYsGS
|
||||
zB8H1Av+MuNxBuQFxNvORGcudExCAAgQZb3ykYNVxPT1CzwVdd16B+VvRyt3+PKz
|
||||
nSsyYyrdvd2xpdaEXFHBA8RxS5ZWCz/hkrdxUhBUWV8O5OUMOHDYvetWc6/9GeuR
|
||||
3dd4VElpLsEs6hIpwnejR1EouNr5OhxstpnB2AOz5N7LWlG5lTKhaHs1zN1uLc4f
|
||||
GmdJZ+5+PYm1UUipFj4kolkI+44Ytl6mj+tTyC4VJAj0mwnXQtp/JdFcDmmeRrTI
|
||||
AwmanJKQlK3yw331FYSd/CXuqCGOh157X7Z5P2Mtr3ZOaNj7qLY0mjweqxjj4fnN
|
||||
YTUu22KhRLGzggEbTg+5huYhtvqa1b87EcH6ukxWoBYQpFK+TyyhuX3ZeT5x6lFi
|
||||
8SP9o/9KcQxB5oD6X5FGMR4v5VDosNnNuqW8G7g4fkcjQKY65tnX75G5Ih156BAP
|
||||
dfZ6+nKCQvXgv1XRymF3UrJeddXtOMQzh4aSvYwwy46qNMPYMeDq6PoMXGVNeylP
|
||||
bTrjLEtE
|
||||
=OvPw
|
||||
iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAmTVE0YACgkQTHxlYsGS
|
||||
zB/3YgwAlpt6B7w//bGGcbi1pGZU90Pz7qwDckvKQHAf1fh6BEKDb7lmFfw2okfI
|
||||
0PIZ5tYiVrPI2FRoNMWUrJhqBvMJ22fi24ZXT4KHPAbm/tC9QmuxWA/sXHlx8EEM
|
||||
7LhIBlBpaOd7F5Qu1PyLgfRWL/k/US4xm54yhpyDf61TiwiHuG/dBaTHTHoDhlKL
|
||||
rTrkXngQ1DbKjOSj9XxVuMS7BMV5QMT0fex3BEA2hSPHdWqt209mgYalxzV6vGp3
|
||||
yjO1uGTfZi0P7S+F+QCZX4DYq5NMwlNEAt0OQbyE4rhI637nkh52mO9GUjezyPyw
|
||||
eFeoUakwhbPMf6wdiBj378Vng+ja2HoN18ZGxmA4/2dF5w6ShlkPJuyjfEsqp8kr
|
||||
A6wnNp+iSf2t2zam8AbN1cfiSYDJgJKcfecdO2gU/Bu6khqr3IPrjuFVR4ObSaV9
|
||||
kVoQo9rcu0gDybDjDHBpwgJwEIVwtEXwVyFe5fC7lK5Pc8oPz4pz6GtCo9OdnPr9
|
||||
dEFcQAtF
|
||||
=w4gX
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
Binary file not shown.
@@ -1,14 +1,14 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAl4soo8ACgkQTHxlYsGS
|
||||
zB+7RwwAmwLH64J1G9LUQRD1rJFlvlIGgEE6leaJO4bKkOACD4QQs70q+rLzNazA
|
||||
maVKjOddKq5fslIsQxHHlIeIMfuTioQ12ZOU6pVY1o1gw5RPolCopLdgac1lA0Fg
|
||||
dDMCtB62iRJqcsnUWFJwhf9fBCwNVGjG6+fPnLyHOXNmDGWDnxxPLNevSEya/Yzg
|
||||
68C1ervBJ4AV6x6Wyc4ejaAliHOBMzv1Z8NclWisKYllaigUUtWauctiM00Ga7kM
|
||||
ADOMVBljI8QauV7E39mxr89Z9ULo8moI2dIPwyrC2vWaW0RnK1XXO2+pDugrlSN7
|
||||
ZLGFMdcD6kjnwaZXfSNXeXh2PeERjhRk8Zw5mvrg51VgUaqv2pZjP4g2hA5D1D0U
|
||||
BGgPLVrlkqEV9my8MNLMhBmj1WfBzDNMEcUu4UbeVpnkj3Px0cnWAAFQgSAMniON
|
||||
c8ng0VEuEHo2z3kiQcBT//mHgFuzaX47OYgPsBPZzMpIAlQ+giryHT8byXcOLcfc
|
||||
KuwzQRZj
|
||||
=Z3mM
|
||||
iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAmTVE0cACgkQTHxlYsGS
|
||||
zB+ikwwAyBpTvmyUT+WEnK8H/Ppdk8bGoPH8CdygOwR3Ss57IEd7lJWSVT2Wqnde
|
||||
3aINnwa5Q6vHPEOiNFA+FiHGOonLlB7a4tqb2/Je/cWYyGSz8Fq+UNp9qyo5s/0s
|
||||
xoDQrraNJIXQ9e+G0IiyL/aN8A+DnJVp9wEFkHDnp0eqDyRwULlK/HWwjnnIOk0K
|
||||
vSae7T610SCjYgLT+S+QZ8/hefzhEYq6S54qN+LL3TdGcveFQNUwgVKcHipCo1s1
|
||||
WffRcC24Rt9LiQd00CAdQCKGoTl3KjxXsU70oeDCXHGvzod0iH/OIww437ewz0EU
|
||||
/f+HVHHJAopbp947KUv5yvlGfqMXjgtDPKDyu4WtiK/BRkX7Wd9+PMLCLjn5URLx
|
||||
9s+c0+5/uBdIxPwBzvdlcogoR0pmGcR1MZwias83mLYuaABsZsDVdNxsRTSVtEzj
|
||||
Ko+2rvw3Yp3xdxPa/lXYFpf74e/oqqcY3qM0BHc+cnWBnTiTc3RfhnyJI98U3RzV
|
||||
G78oFS/3
|
||||
=Y1mv
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user