diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 5e793992ce..de750130d8 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -87,6 +87,7 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent *memoryConfig.ServiceSettings.LocalModeSocketLocation = filepath.Join(tempWorkspace, "mattermost_local.sock") *memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false *memoryConfig.AnnouncementSettings.UserNoticesEnabled = false + *memoryConfig.PluginSettings.AutomaticPrepackagedPlugins = false if updateConfig != nil { updateConfig(memoryConfig) } diff --git a/app/helper_test.go b/app/helper_test.go index d4643fbb63..8e3f97452a 100644 --- a/app/helper_test.go +++ b/app/helper_test.go @@ -56,6 +56,7 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo } *config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins") *config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp") + *config.PluginSettings.AutomaticPrepackagedPlugins = false *config.LogSettings.EnableSentry = false // disable error reporting during tests *config.AnnouncementSettings.AdminNoticesEnabled = false *config.AnnouncementSettings.UserNoticesEnabled = false diff --git a/app/plugin.go b/app/plugin.go index 5d59c30b72..94d59ec013 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -198,10 +198,13 @@ func (a *App) InitPlugins(pluginDir, webappPluginDir string) { } pluginsEnvironment.SetPrepackagedPlugins(plugins) + a.installFeatureFlagPlugins() + // Sync plugin active state when config changes. Also notify plugins. a.Srv().PluginsLock.Lock() a.RemoveConfigListener(a.Srv().PluginConfigListenerId) a.Srv().PluginConfigListenerId = a.AddConfigListener(func(*model.Config, *model.Config) { + a.installFeatureFlagPlugins() a.SyncPluginsActiveState() if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { pluginsEnvironment.RunMultiPluginHook(func(hooks plugin.Hooks) bool { @@ -510,7 +513,8 @@ func (a *App) getRemoteMarketplacePlugin(pluginId, version string) (*model.BaseM } filter := a.getBaseMarketplaceFilter() - filter.Filter = pluginId + filter.PluginId = pluginId + filter.ReturnAllVersions = true plugin, err := marketplaceClient.GetPlugin(filter, version) if err != nil { @@ -886,6 +890,47 @@ func (a *App) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*plugin return plugin, nil } +// installFeatureFlagPlugins handles the automatic installation/upgrade of plugins from feature flags +func (a *App) installFeatureFlagPlugins() { + ffControledPlugins := a.Config().FeatureFlags.Plugins() + + // Respect the automatic prepackaged disable setting + if !*a.Config().PluginSettings.AutomaticPrepackagedPlugins { + return + } + + for pluginId, version := range ffControledPlugins { + // Skip installing if the plugin has been previously disabled. + pluginState := a.Config().PluginSettings.PluginStates[pluginId] + if pluginState != nil && !pluginState.Enable { + a.Log().Debug("Not auto installing/upgrade because plugin was disabled", mlog.String("plugin_id", pluginId), mlog.String("version", version)) + continue + } + + // Check if we already installed this version as InstallMarketplacePlugin can't handle re-installs well. + pluginStatus, err := a.Srv().GetPluginStatus(pluginId) + if err == nil && pluginStatus.Version == version { + continue + } + + if version != "" && version != "control" { + _, err := a.InstallMarketplacePlugin(&model.InstallMarketplacePluginRequest{ + Id: pluginId, + Version: version, + }) + if err != nil { + a.Log().Debug("Unable to install plugin from FF manifest", mlog.String("plugin_id", pluginId), mlog.Err(err), mlog.String("version", version)) + } else { + if err := a.EnablePlugin(pluginId); err != nil { + a.Log().Debug("Unable to enable plugin installed from feature flag.", mlog.String("plugin_id", pluginId), mlog.Err(err), mlog.String("version", version)) + } else { + a.Log().Debug("Installed and enabled plugin.", mlog.String("plugin_id", pluginId), mlog.String("version", version)) + } + } + } + } +} + // getPrepackagedPlugin builds a PrepackagedPlugin from the plugin at the given path, additionally returning the directory in which it was extracted. func getPrepackagedPlugin(pluginPath *pluginSignaturePath, pluginFile io.ReadSeeker, tmpDir string) (*plugin.PrepackagedPlugin, string, error) { manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir) diff --git a/model/feature_flags.go b/model/feature_flags.go index eee5c7dbac..59b407bfa6 100644 --- a/model/feature_flags.go +++ b/model/feature_flags.go @@ -3,6 +3,8 @@ package model +import "reflect" + type FeatureFlags struct { // Exists only for unit and manual testing. // When set to a value, will be returned by the ping endpoint. @@ -13,10 +15,35 @@ type FeatureFlags struct { // Toggle on and off scheduled jobs for cloud user limit emails see MM-29999 CloudDelinquentEmailJobsEnabled bool + + // Feature flags to control plugin versions + PluginIncidentManagement string `plugin_id:"com.mattermost.plugin-incident-management"` } func (f *FeatureFlags) SetDefaults() { f.TestFeature = "off" f.TestBoolFeature = false f.CloudDelinquentEmailJobsEnabled = false + + f.PluginIncidentManagement = "1.1.1" +} + +func (f *FeatureFlags) Plugins() map[string]string { + rFFVal := reflect.ValueOf(f).Elem() + rFFType := reflect.TypeOf(f).Elem() + + pluginVersions := make(map[string]string) + for i := 0; i < rFFVal.NumField(); i++ { + rFieldVal := rFFVal.Field(i) + rFieldType := rFFType.Field(i) + + pluginId, hasPluginId := rFieldType.Tag.Lookup("plugin_id") + if !hasPluginId { + continue + } + + pluginVersions[pluginId] = rFieldVal.String() + } + + return pluginVersions } diff --git a/model/marketplace_plugin.go b/model/marketplace_plugin.go index 16df0b2fde..cad93dfb03 100644 --- a/model/marketplace_plugin.go +++ b/model/marketplace_plugin.go @@ -87,6 +87,8 @@ type MarketplacePluginFilter struct { Cloud bool LocalOnly bool Platform string + PluginId string + ReturnAllVersions bool } // ApplyToURL modifies the given url to include query string parameters for the request. @@ -103,6 +105,8 @@ func (filter *MarketplacePluginFilter) ApplyToURL(u *url.URL) { q.Add("cloud", strconv.FormatBool(filter.Cloud)) q.Add("local_only", strconv.FormatBool(filter.LocalOnly)) q.Add("platform", filter.Platform) + q.Add("plugin_id", filter.PluginId) + q.Add("return_all_versions", strconv.FormatBool(filter.ReturnAllVersions)) u.RawQuery = q.Encode() } diff --git a/web/web_test.go b/web/web_test.go index 6f7d041933..a08c78ee89 100644 --- a/web/web_test.go +++ b/web/web_test.go @@ -72,6 +72,7 @@ func setupTestHelper(t testing.TB, store store.Store, includeCacheLayer bool) *T newConfig := memoryStore.Get().Clone() *newConfig.AnnouncementSettings.AdminNoticesEnabled = false *newConfig.AnnouncementSettings.UserNoticesEnabled = false + *newConfig.PluginSettings.AutomaticPrepackagedPlugins = false memoryStore.Set(newConfig) var options []app.Option options = append(options, app.ConfigStore(memoryStore))