Plugins: Make it possible to support multiple plugin versions (#82116)
* first pass * use version in more places * add comment * update installer * fix wire * fix tests * tidy * simplify changes * fix in mem * remove unused step * fix step dupe logic for child plugins + add tests
This commit is contained in:
@@ -89,8 +89,8 @@ func NewMetricsMiddleware(promRegisterer prometheus.Registerer, pluginRegistry r
|
||||
}
|
||||
|
||||
// pluginTarget returns the value for the "target" Prometheus label for the given plugin ID.
|
||||
func (m *MetricsMiddleware) pluginTarget(ctx context.Context, pluginID string) (string, error) {
|
||||
p, exists := m.pluginRegistry.Plugin(ctx, pluginID)
|
||||
func (m *MetricsMiddleware) pluginTarget(ctx context.Context, pluginID, pluginVersion string) (string, error) {
|
||||
p, exists := m.pluginRegistry.Plugin(ctx, pluginID, pluginVersion)
|
||||
if !exists {
|
||||
return "", plugins.ErrPluginNotRegistered
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func (m *MetricsMiddleware) pluginTarget(ctx context.Context, pluginID string) (
|
||||
|
||||
// instrumentPluginRequestSize tracks the size of the given request in the m.pluginRequestSize metric.
|
||||
func (m *MetricsMiddleware) instrumentPluginRequestSize(ctx context.Context, pluginCtx backend.PluginContext, endpoint string, requestSize float64) error {
|
||||
target, err := m.pluginTarget(ctx, pluginCtx.PluginID)
|
||||
target, err := m.pluginTarget(ctx, pluginCtx.PluginID, pluginCtx.PluginVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func (m *MetricsMiddleware) instrumentPluginRequestSize(ctx context.Context, plu
|
||||
|
||||
// instrumentPluginRequest increments the m.pluginRequestCounter metric and tracks the duration of the given request.
|
||||
func (m *MetricsMiddleware) instrumentPluginRequest(ctx context.Context, pluginCtx backend.PluginContext, endpoint string, fn func(context.Context) (requestStatus, error)) error {
|
||||
target, err := m.pluginTarget(ctx, pluginCtx.PluginID)
|
||||
target, err := m.pluginTarget(ctx, pluginCtx.PluginID, pluginCtx.PluginVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ func ProvideFileStoreManager(pluginStore pluginstore.Store, pluginFileStore plug
|
||||
}
|
||||
}
|
||||
|
||||
var openDashboardFile = func(ctx context.Context, pluginFileStore plugins.FileStore, pluginID, name string) (*plugins.File, error) {
|
||||
f, err := pluginFileStore.File(ctx, pluginID, name)
|
||||
var openDashboardFile = func(ctx context.Context, pluginFileStore plugins.FileStore, pluginID, pluginVersion, name string) (*plugins.File, error) {
|
||||
f, err := pluginFileStore.File(ctx, pluginID, pluginVersion, name)
|
||||
if err != nil {
|
||||
return &plugins.File{}, err
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func (m *FileStoreManager) GetPluginDashboardFileContents(ctx context.Context, a
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := openDashboardFile(ctx, m.pluginFileStore, plugin.ID, cleanPath)
|
||||
file, err := openDashboardFile(ctx, m.pluginFileStore, plugin.ID, plugin.Info.Version, cleanPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ func TestDashboardFileStore(t *testing.T) {
|
||||
Data: []byte("dash2"),
|
||||
},
|
||||
}
|
||||
openDashboardFile = func(ctx context.Context, pluginFiles plugins.FileStore, pluginID, name string) (*plugins.File, error) {
|
||||
openDashboardFile = func(ctx context.Context, pluginFiles plugins.FileStore, pluginID, _, name string) (*plugins.File, error) {
|
||||
f, err := mapFs.Open(name)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -1666,7 +1666,7 @@ func verifyState(t *testing.T, ps []*plugins.Plugin, reg registry.Service,
|
||||
t.Helper()
|
||||
|
||||
for _, p := range ps {
|
||||
regP, exists := reg.Plugin(context.Background(), p.ID)
|
||||
regP, exists := reg.Plugin(context.Background(), p.ID, p.Info.Version)
|
||||
require.True(t, exists)
|
||||
if !cmp.Equal(p, regP, compareOpts...) {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, regP, compareOpts...))
|
||||
|
||||
@@ -29,7 +29,7 @@ func ProvideDiscoveryStage(cfg *config.Cfg, pf finder.Finder, pr registry.Servic
|
||||
plugins.TypeDataSource, plugins.TypeApp, plugins.TypePanel, plugins.TypeSecretsManager,
|
||||
}),
|
||||
func(ctx context.Context, _ plugins.Class, b []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) {
|
||||
return discovery.NewDuplicatePluginFilterStep(pr).Filter(ctx, b)
|
||||
return NewDuplicatePluginIDFilterStep(pr).Filter(ctx, b)
|
||||
},
|
||||
func(_ context.Context, _ plugins.Class, b []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) {
|
||||
return NewDisablePluginsStep(cfg).Filter(b)
|
||||
|
||||
@@ -3,6 +3,7 @@ package pipeline
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/validation"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -195,3 +197,54 @@ func (c *AsExternal) Filter(cl plugins.Class, bundles []*plugins.FoundBundle) ([
|
||||
}
|
||||
return bundles, nil
|
||||
}
|
||||
|
||||
// DuplicatePluginIDValidation is a filter step that will filter out any plugins that are already registered with the same
|
||||
// plugin ID. This includes both the primary plugin and child plugins, which are matched using the plugin.json plugin
|
||||
// ID field.
|
||||
type DuplicatePluginIDValidation struct {
|
||||
registry registry.Service
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// NewDuplicatePluginIDFilterStep returns a new DuplicatePluginIDValidation.
|
||||
func NewDuplicatePluginIDFilterStep(registry registry.Service) *DuplicatePluginIDValidation {
|
||||
return &DuplicatePluginIDValidation{
|
||||
registry: registry,
|
||||
log: log.New("plugins.dedupe"),
|
||||
}
|
||||
}
|
||||
|
||||
// Filter will filter out any plugins that have already been registered under the same plugin ID.
|
||||
func (d *DuplicatePluginIDValidation) Filter(ctx context.Context, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) {
|
||||
res := make([]*plugins.FoundBundle, 0, len(bundles))
|
||||
|
||||
var matchesPluginIDFunc = func(fp plugins.FoundPlugin) func(p *plugins.Plugin) bool {
|
||||
return func(p *plugins.Plugin) bool {
|
||||
return p.ID == fp.JSONData.ID
|
||||
}
|
||||
}
|
||||
|
||||
for _, b := range bundles {
|
||||
ps := d.registry.Plugins(ctx)
|
||||
|
||||
if slices.ContainsFunc(ps, matchesPluginIDFunc(b.Primary)) {
|
||||
d.log.Warn("Skipping loading of plugin as it's a duplicate", "pluginId", b.Primary.JSONData.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
var nonDupeChildren []*plugins.FoundPlugin
|
||||
for _, child := range b.Children {
|
||||
if slices.ContainsFunc(ps, matchesPluginIDFunc(*child)) {
|
||||
d.log.Warn("Skipping loading of child plugin as it's a duplicate", "pluginId", child.JSONData.ID)
|
||||
continue
|
||||
}
|
||||
nonDupeChildren = append(nonDupeChildren, child)
|
||||
}
|
||||
res = append(res, &plugins.FoundBundle{
|
||||
Primary: b.Primary,
|
||||
Children: nonDupeChildren,
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/config"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@@ -82,3 +84,98 @@ func TestAsExternal(t *testing.T) {
|
||||
require.Equal(t, filtered[0].Primary.JSONData.ID, "plugin2")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDuplicatePluginIDValidation(t *testing.T) {
|
||||
tcs := []struct {
|
||||
name string
|
||||
registeredPlugins []string
|
||||
in []*plugins.FoundBundle
|
||||
out []*plugins.FoundBundle
|
||||
}{
|
||||
{
|
||||
name: "should filter out a plugin if it already exists in the plugin registry",
|
||||
registeredPlugins: []string{"foobar-datasource"},
|
||||
in: []*plugins.FoundBundle{
|
||||
{
|
||||
Primary: plugins.FoundPlugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "foobar-datasource",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
out: []*plugins.FoundBundle{},
|
||||
},
|
||||
{
|
||||
name: "should not filter out a plugin if it doesn't exist in the plugin registry",
|
||||
registeredPlugins: []string{"foobar-datasource"},
|
||||
in: []*plugins.FoundBundle{
|
||||
{
|
||||
Primary: plugins.FoundPlugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-datasource",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
out: []*plugins.FoundBundle{
|
||||
{
|
||||
Primary: plugins.FoundPlugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-datasource",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "should filter out child plugins if they are already registered",
|
||||
registeredPlugins: []string{"foobar-datasource"},
|
||||
in: []*plugins.FoundBundle{
|
||||
{
|
||||
Primary: plugins.FoundPlugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-datasource",
|
||||
},
|
||||
},
|
||||
Children: []*plugins.FoundPlugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "foobar-datasource",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
out: []*plugins.FoundBundle{
|
||||
{
|
||||
Primary: plugins.FoundPlugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-datasource",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := registry.NewInMemory()
|
||||
s := NewDuplicatePluginIDFilterStep(r)
|
||||
|
||||
ctx := context.Background()
|
||||
for _, pluginID := range tc.registeredPlugins {
|
||||
err := r.Add(ctx, &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: pluginID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
res, err := s.Filter(ctx, tc.in)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.out, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||
@@ -100,7 +99,7 @@ func TestIntegrationPluginManager(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
verifyCorePluginCatalogue(t, ctx, testCtx.PluginStore)
|
||||
verifyBundledPlugins(t, ctx, testCtx.PluginStore)
|
||||
verifyPluginStaticRoutes(t, ctx, testCtx.PluginStore, testCtx.PluginRegistry)
|
||||
verifyPluginStaticRoutes(t, ctx, testCtx.PluginStore, testCtx.PluginStore)
|
||||
verifyBackendProcesses(t, testCtx.PluginRegistry.Plugins(ctx))
|
||||
verifyPluginQuery(t, ctx, testCtx.PluginClient)
|
||||
}
|
||||
@@ -253,7 +252,7 @@ func verifyBundledPlugins(t *testing.T, ctx context.Context, ps *pluginstore.Ser
|
||||
}
|
||||
}
|
||||
|
||||
func verifyPluginStaticRoutes(t *testing.T, ctx context.Context, rr plugins.StaticRouteResolver, reg registry.Service) {
|
||||
func verifyPluginStaticRoutes(t *testing.T, ctx context.Context, rr plugins.StaticRouteResolver, ps *pluginstore.Service) {
|
||||
routes := make(map[string]*plugins.StaticRoute)
|
||||
for _, route := range rr.Routes(ctx) {
|
||||
routes[route.PluginID] = route
|
||||
@@ -261,13 +260,13 @@ func verifyPluginStaticRoutes(t *testing.T, ctx context.Context, rr plugins.Stat
|
||||
|
||||
require.Len(t, routes, 2)
|
||||
|
||||
inputPlugin, _ := reg.Plugin(ctx, "input")
|
||||
inputPlugin, _ := ps.Plugin(ctx, "input")
|
||||
require.NotNil(t, routes["input"])
|
||||
require.Equal(t, routes["input"].Directory, inputPlugin.FS.Base())
|
||||
require.Equal(t, routes["input"].Directory, inputPlugin.Base())
|
||||
|
||||
testAppPlugin, _ := reg.Plugin(ctx, "test-app")
|
||||
testAppPlugin, _ := ps.Plugin(ctx, "test-app")
|
||||
require.Contains(t, routes, "test-app")
|
||||
require.Equal(t, routes["test-app"].Directory, testAppPlugin.FS.Base())
|
||||
require.Equal(t, routes["test-app"].Directory, testAppPlugin.Base())
|
||||
}
|
||||
|
||||
func verifyBackendProcesses(t *testing.T, ps []*plugins.Plugin) {
|
||||
|
||||
@@ -18,6 +18,7 @@ var _ Store = (*Service)(nil)
|
||||
// Store is the publicly accessible storage for plugins.
|
||||
type Store interface {
|
||||
// Plugin finds a plugin by its ID.
|
||||
// Note: version is not required since Grafana only supports single versions of a plugin.
|
||||
Plugin(ctx context.Context, pluginID string) (Plugin, bool)
|
||||
// Plugins returns plugins by their requested type.
|
||||
Plugins(ctx context.Context, pluginTypes ...plugins.Type) []Plugin
|
||||
@@ -104,7 +105,7 @@ func (s *Service) SecretsManager(ctx context.Context) *plugins.Plugin {
|
||||
|
||||
// plugin finds a plugin with `pluginID` from the registry that is not decommissioned
|
||||
func (s *Service) plugin(ctx context.Context, pluginID string) (*plugins.Plugin, bool) {
|
||||
p, exists := s.pluginRegistry.Plugin(ctx, pluginID)
|
||||
p, exists := s.pluginRegistry.Plugin(ctx, pluginID, "") // version is not required since Grafana only supports single versions of a plugin
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
)
|
||||
|
||||
@@ -106,7 +107,7 @@ func createLoader(cfg *config.Cfg, pr registry.Service, l plugins.Licensing) (lo
|
||||
FindFilterFuncs: []discovery.FindFilterFunc{
|
||||
discovery.NewPermittedPluginTypesFilterStep([]plugins.Type{plugins.TypeRenderer}),
|
||||
func(ctx context.Context, class plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) {
|
||||
return discovery.NewDuplicatePluginFilterStep(pr).Filter(ctx, bundles)
|
||||
return pipeline.NewDuplicatePluginIDFilterStep(pr).Filter(ctx, bundles)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user