From 61cdfba87a36bd4b8e1bb227a14c5d88fc943aff Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Fri, 22 Sep 2023 10:50:13 +0200 Subject: [PATCH] Feature: Allow to load a core plugin as external (#75157) --- .../setup-grafana/configure-grafana/_index.md | 6 ++ .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 6 ++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++ .../pluginsintegration/pipeline/pipeline.go | 3 + .../pluginsintegration/pipeline/steps.go | 58 ++++++++++++++++++ .../pluginsintegration/pipeline/steps_test.go | 59 +++++++++++++++++++ 9 files changed, 139 insertions(+) diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 1bfb08672d5..4b75b9891e2 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -2195,6 +2195,12 @@ Available in Grafana v9.5.0 or later, and [OpenTelemetry must be configured as w If `true`, propagate the tracing context to the plugin backend and enable tracing (if the backend supports it). +## as_external + +Load an external version of a core plugin if it has been installed. + +Experimental. Requires the feature toggle `externalCorePlugins` to be enabled. +
## [plugin.grafana-image-renderer] diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 96fb0128294..f98d9a05e76 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -136,6 +136,7 @@ Experimental features might be changed or removed without prior notice. | `requestInstrumentationStatusSource` | Include a status source label for request metrics and logs | | `wargamesTesting` | Placeholder feature flag for internal testing | | `alertingInsights` | Show the new alerting insights landing page | +| `externalCorePlugins` | Allow core plugins to be loaded as external | | `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | ## Development feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index ea13fc0f721..d9aaca3f322 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -127,5 +127,6 @@ export interface FeatureToggles { lokiRunQueriesInParallel?: boolean; wargamesTesting?: boolean; alertingInsights?: boolean; + externalCorePlugins?: boolean; pluginsAPIMetrics?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index caa4724d8b3..f28c6bf5c7c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -759,6 +759,12 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaAlertingSquad, }, + { + Name: "externalCorePlugins", + Description: "Allow core plugins to be loaded as external", + Stage: FeatureStageExperimental, + Owner: grafanaPluginsPlatformSquad, + }, { Name: "pluginsAPIMetrics", Description: "Sends metrics of public grafana packages usage by plugins", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index b50154601ac..e98f7f74cb1 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -108,4 +108,5 @@ requestInstrumentationStatusSource,experimental,@grafana/plugins-platform-backen lokiRunQueriesInParallel,privatePreview,@grafana/observability-logs,false,false,false,false wargamesTesting,experimental,@grafana/hosted-grafana-team,false,false,false,false alertingInsights,experimental,@grafana/alerting-squad,false,false,false,true +externalCorePlugins,experimental,@grafana/plugins-platform-backend,false,false,false,false pluginsAPIMetrics,experimental,@grafana/plugins-platform-backend,false,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index e04185c4814..3f6d18f77c1 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -443,6 +443,10 @@ const ( // Show the new alerting insights landing page FlagAlertingInsights = "alertingInsights" + // FlagExternalCorePlugins + // Allow core plugins to be loaded as external + FlagExternalCorePlugins = "externalCorePlugins" + // FlagPluginsAPIMetrics // Sends metrics of public grafana packages usage by plugins FlagPluginsAPIMetrics = "pluginsAPIMetrics" diff --git a/pkg/services/pluginsintegration/pipeline/pipeline.go b/pkg/services/pluginsintegration/pipeline/pipeline.go index 2656bf5936c..5d1e0a02b1b 100644 --- a/pkg/services/pluginsintegration/pipeline/pipeline.go +++ b/pkg/services/pluginsintegration/pipeline/pipeline.go @@ -33,6 +33,9 @@ func ProvideDiscoveryStage(cfg *config.Cfg, pf finder.Finder, pr registry.Servic func(_ context.Context, _ plugins.Class, b []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { return NewDisablePluginsStep(cfg).Filter(b) }, + func(_ context.Context, c plugins.Class, b []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { + return NewAsExternalStep(cfg).Filter(c, b) + }, }, }) } diff --git a/pkg/services/pluginsintegration/pipeline/steps.go b/pkg/services/pluginsintegration/pipeline/steps.go index 34b4eef139b..50e710a1743 100644 --- a/pkg/services/pluginsintegration/pipeline/steps.go +++ b/pkg/services/pluginsintegration/pipeline/steps.go @@ -157,3 +157,61 @@ func (c *DisablePlugins) Filter(bundles []*plugins.FoundBundle) ([]*plugins.Foun } return res, nil } + +// AsExternal is a filter step that will skip loading a core plugin to use an external one. +type AsExternal struct { + log log.Logger + cfg *config.Cfg +} + +// NewDisablePluginsStep returns a new DisablePlugins. +func NewAsExternalStep(cfg *config.Cfg) *AsExternal { + return &AsExternal{ + cfg: cfg, + log: log.New("plugins.asExternal"), + } +} + +// Filter will filter out any plugins that are marked to be disabled. +func (c *AsExternal) Filter(cl plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { + if c.cfg.Features == nil || !c.cfg.Features.IsEnabled(featuremgmt.FlagExternalCorePlugins) { + return bundles, nil + } + + if cl == plugins.ClassCore { + res := []*plugins.FoundBundle{} + for _, bundle := range bundles { + pluginCfg := c.cfg.PluginSettings[bundle.Primary.JSONData.ID] + // Skip core plugins if the feature flag is enabled and the plugin is in the skip list. + // It could be loaded later as an external plugin. + if pluginCfg["as_external"] == "true" { + c.log.Debug("Skipping the core plugin load", "pluginID", bundle.Primary.JSONData.ID) + } else { + res = append(res, bundle) + } + } + return res, nil + } + + if cl == plugins.ClassExternal { + // Warn if the plugin is not found in the external plugins directory. + asExternal := map[string]bool{} + for pluginID, pluginCfg := range c.cfg.PluginSettings { + if pluginCfg["as_external"] == "true" { + asExternal[pluginID] = true + } + } + for _, bundle := range bundles { + if asExternal[bundle.Primary.JSONData.ID] { + delete(asExternal, bundle.Primary.JSONData.ID) + } + } + if len(asExternal) > 0 { + for p := range asExternal { + c.log.Error("Core plugin expected to be loaded as external, but it is missing", "pluginID", p) + } + } + } + + return bundles, nil +} diff --git a/pkg/services/pluginsintegration/pipeline/steps_test.go b/pkg/services/pluginsintegration/pipeline/steps_test.go index 0b4f1b3f0b2..98c665d0d10 100644 --- a/pkg/services/pluginsintegration/pipeline/steps_test.go +++ b/pkg/services/pluginsintegration/pipeline/steps_test.go @@ -5,6 +5,9 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" ) @@ -43,3 +46,59 @@ func TestSkipPlugins(t *testing.T) { require.Len(t, filtered, 1) require.Equal(t, filtered[0].Primary.JSONData.ID, "plugin3") } + +func TestAsExternal(t *testing.T) { + bundles := []*plugins.FoundBundle{ + { + Primary: plugins.FoundPlugin{ + JSONData: plugins.JSONData{ + ID: "plugin1", + }, + }, + }, + { + Primary: plugins.FoundPlugin{ + JSONData: plugins.JSONData{ + ID: "plugin2", + }, + }, + }, + } + + t.Run("should skip a core plugin", func(t *testing.T) { + cfg := &config.Cfg{ + Features: featuremgmt.WithFeatures(featuremgmt.FlagExternalCorePlugins), + PluginSettings: setting.PluginSettings{ + "plugin1": map[string]string{ + "as_external": "true", + }, + }, + } + + s := NewAsExternalStep(cfg) + filtered, err := s.Filter(plugins.ClassCore, bundles) + require.NoError(t, err) + require.Len(t, filtered, 1) + require.Equal(t, filtered[0].Primary.JSONData.ID, "plugin2") + }) + + t.Run("should log an error if an external plugin is not available", func(t *testing.T) { + cfg := &config.Cfg{ + Features: featuremgmt.WithFeatures(featuremgmt.FlagExternalCorePlugins), + PluginSettings: setting.PluginSettings{ + "plugin3": map[string]string{ + "as_external": "true", + }, + }, + } + + fakeLogger := log.NewTestLogger() + s := NewAsExternalStep(cfg) + s.log = fakeLogger + + filtered, err := s.Filter(plugins.ClassExternal, bundles) + require.NoError(t, err) + require.Len(t, filtered, 2) + require.Equal(t, fakeLogger.ErrorLogs.Calls, 1) + }) +}