From ad142c958e32d3222c5e4560b8cdc63b4b736691 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Thu, 17 Aug 2023 12:46:57 -0300 Subject: [PATCH] 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> --- server/channels/api4/plugin_test.go | 4 +- server/channels/app/channels.go | 11 +- server/channels/app/plugin.go | 234 +++++++- server/channels/app/plugin_install.go | 151 +++-- server/channels/app/plugin_install_test.go | 6 +- server/channels/app/plugin_test.go | 642 ++++++++++++++++----- server/public/plugin/environment.go | 48 +- server/tests/README.md | 4 +- server/tests/testplugin-v0.0.2.tar.gz | Bin 0 -> 73048 bytes server/tests/testplugin-v0.0.2.tar.gz.asc | 14 + server/tests/testplugin-v0.0.2.tar.gz.sig | Bin 0 -> 438 bytes server/tests/testplugin.tar.gz.asc | 22 +- server/tests/testplugin.tar.gz.sig | Bin 438 -> 438 bytes server/tests/testplugin2.tar.gz.asc | 22 +- server/tests/testplugin2.tar.gz.sig | Bin 438 -> 438 bytes 15 files changed, 908 insertions(+), 250 deletions(-) create mode 100644 server/tests/testplugin-v0.0.2.tar.gz create mode 100644 server/tests/testplugin-v0.0.2.tar.gz.asc create mode 100644 server/tests/testplugin-v0.0.2.tar.gz.sig diff --git a/server/channels/api4/plugin_test.go b/server/channels/api4/plugin_test.go index bcc4b19c80..b515917ca9 100644 --- a/server/channels/api4/plugin_test.go +++ b/server/channels/api4/plugin_test.go @@ -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) diff --git a/server/channels/app/channels.go b/server/channels/app/channels.go index 7e5f94508f..76b6858f19 100644 --- a/server/channels/app/channels.go +++ b/server/channels/app/channels.go @@ -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 diff --git a/server/channels/app/plugin.go b/server/channels/app/plugin.go index 51da55dc43..e4ce41b7e3 100644 --- a/server/channels/app/plugin.go +++ b/server/channels/app/plugin.go @@ -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) diff --git a/server/channels/app/plugin_install.go b/server/channels/app/plugin_install.go index f1967188ce..75ab7d4cd6 100644 --- a/server/channels/app/plugin_install.go +++ b/server/channels/app/plugin_install.go @@ -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) diff --git a/server/channels/app/plugin_install_test.go b/server/channels/app/plugin_install_test.go index c3e0b31c91..d25e41bd43 100644 --- a/server/channels/app/plugin_install_test.go +++ b/server/channels/app/plugin_install_test.go @@ -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) } diff --git a/server/channels/app/plugin_test.go b/server/channels/app/plugin_test.go index 74be41170e..34aa0d06f6 100644 --- a/server/channels/app/plugin_test.go +++ b/server/channels/app/plugin_test.go @@ -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") }) } diff --git a/server/public/plugin/environment.go b/server/public/plugin/environment.go index 32fd819a55..acf808dea3 100644 --- a/server/public/plugin/environment.go +++ b/server/public/plugin/environment.go @@ -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() } diff --git a/server/tests/README.md b/server/tests/README.md index d259132da5..60481b4143 100644 --- a/server/tests/README.md +++ b/server/tests/README.md @@ -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 ``` diff --git a/server/tests/testplugin-v0.0.2.tar.gz b/server/tests/testplugin-v0.0.2.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..5629ced271acb0db3231a9ff71469385ba63b05e GIT binary patch literal 73048 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 S_V3^4-~R*GV(U2o$OHh$xkH5j literal 0 HcmV?d00001 diff --git a/server/tests/testplugin-v0.0.2.tar.gz.asc b/server/tests/testplugin-v0.0.2.tar.gz.asc new file mode 100644 index 0000000000..9f16407103 --- /dev/null +++ b/server/tests/testplugin-v0.0.2.tar.gz.asc @@ -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----- diff --git a/server/tests/testplugin-v0.0.2.tar.gz.sig b/server/tests/testplugin-v0.0.2.tar.gz.sig new file mode 100644 index 0000000000000000000000000000000000000000..47880e6a960374289d7eaff182264cbbaddb9c04 GIT binary patch literal 438 zcmV;n0ZIOe0kZ@E0SEvc79j-l`p!k*-efF`)~M!8d}U(6lFT0k0%X+_MgR&45KMe! zV!@KkAKr)y|5uaN0GF&Z!gBYbv8$BzP~py9-jVE=BV)4)!ml@$CT6PLQ*z+BzV!oC{xANW6}FAXgDR-U|StKSzPdnFP~3E_22U9 z0YjE;k+5Slt&`me+Vz)evWiD7x9=ph+*%%xBbw^IMk$4~*7_Kxy-AVgM{gK7- z|IncHD6u|gWvk&X!G!JL{$LnGh`zOO3fy{ek0?tJNP;UX?`*s4zwL7>6@CVS!z}LW z@moG9)+kE$UNA$(iw(vol|kyxnQ%G82cj{qwrGW3>WkOAJA)Pz#S;e(W}ZIcJ(_!B z?Zri{CCeuZGUBUyoC2IrVh{eLd{h#|-5uW=ZFvExuPEbo;OP5N9QcI?&|R}*(O0-W gXsYv=Q_)q~WCcQ|>~OMQj?V!Gd7AMD)1u*C2blK4 zmq@qW&plZrpqOmvIV}|xI2eNwcn~|g6kc0W2xjBHuG{94BUd|_{4#z(&HjZRuV-=Y z0iL~+S-nS8i^=z_8H@o|WAWAOZmx z;zL5*E9b!)H6(Rz5m;8Ka@((eROjqy;tVq%K`EWUU2I11kAfgV{f$xMD;=IPf%8K~ z=d!_=K+Yy5zJEif%1C~fks43_J~WL;QDzY&+@~&}kzrakugvSBScljLPnf5l7m_03 zD&uaH0=2ITJ)E20mWaA!EHJ$X1ROoARo2hD_qeXc=Ep=#Va(q$j8SvNpF delta 414 zcmV;P0b%~O1GWQ@B7b9=MR))T2@p(tWn#gS%pa<`3;P7g(2MH*rbh%?ZAhuM+7Q&a zsaw=ZHMy31%HPyeDwZ*7WpyxPB38Ai1k@S}AfjIaI9qYaS0;4ad(eL0ygDPnIDx*( zRCxqj3r($xT5kx0uDv(ijI-0T_K1>x66fZeaAxHF0uDx`@qaU;Hp8uz@8ry}i-|$t zdNhYU7FIi|14-Cv%+g0cKxtqvsv%TMzy`qWYPXH$vB)4H^dw#}K3+{52=Q;d!j4j5 zTj+T=vDgMxG;+)&8aT0&JK*XjBtvq8!&=eZy;`lunkc>h5KZRiy;;=p+i|t6Ef2kG zW_s0)l;?(bSbx;w^3Hy*C^+&<4+p%cbr^Y4{Y>9z0bGlqD-h(XC|h;pheVL5jWm0w ztc+g|LnO+SL9)DN)A%W1o%^XPck|>$L~$-7e71hs{pWlj)p7*r@!wvraTFSSWRY87 z7ZdbxV;Ff!$(%h)v@jig-bOdz%9ubtu}L**iQ+i7pBH`!#G*CVkA;vn1%ak74;@X?r z@?QPLn8;+a@LIl5+I_1kIkWvN#(>bSwv8l((Dm{NSFuj?q2R(?aj(vYbclb> zBMdm-x3JGa6o38yhg5ON0*YIw-a9Ev`O5i5exnzT3qw4h^1Fqth_As$Mf+Lbemug$ zE;;#867lxWoYU@qxEDyo`~lAQWpaopM@nWH#C0*8B5Kb!n6@rx0BmN!)pXo!MKqPP zOyeq#wyyj)Vx94K6Wab&*cO-j;qU0Grx@O&Gz51(au;=hoj8+obYF&ii6h?>-5k{$ IzbF+i_i|CuV*mgE delta 414 zcmV;P0b%~O1GWQ@B7a^iqLKg#2@p(tWn#gS%pay83;?@%zCf~Lhm^@DxR}P_8BoO| zGiE)n;V&@Fyd!?jMd`z2Sh5~5KAg!p!f<5LVz`P$k#kX{~4y~%e9_lsG;qi|qg z7+11}1-&ME`Q<5gDd zaF6V^l8bhu78Rp|5jhK;mdjKNV6E&u=k