Feature: Allow to load a core plugin as external (#75157)

This commit is contained in:
Andres Martinez Gotor 2023-09-22 10:50:13 +02:00 committed by GitHub
parent e0659c05da
commit 61cdfba87a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 139 additions and 0 deletions

View File

@ -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.
<hr>
## [plugin.grafana-image-renderer]

View File

@ -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

View File

@ -127,5 +127,6 @@ export interface FeatureToggles {
lokiRunQueriesInParallel?: boolean;
wargamesTesting?: boolean;
alertingInsights?: boolean;
externalCorePlugins?: boolean;
pluginsAPIMetrics?: boolean;
}

View File

@ -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",

View File

@ -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

1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
108 lokiRunQueriesInParallel privatePreview @grafana/observability-logs false false false false
109 wargamesTesting experimental @grafana/hosted-grafana-team false false false false
110 alertingInsights experimental @grafana/alerting-squad false false false true
111 externalCorePlugins experimental @grafana/plugins-platform-backend false false false false
112 pluginsAPIMetrics experimental @grafana/plugins-platform-backend false false false true

View File

@ -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"

View File

@ -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)
},
},
})
}

View File

@ -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
}

View File

@ -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)
})
}