move loading strategy into pkg/plugins

This commit is contained in:
Will Browne
2026-01-13 15:46:34 +00:00
parent f5aa39cc27
commit ae06690681
18 changed files with 208 additions and 247 deletions
@@ -127,6 +127,7 @@ func TestLoader_Load(t *testing.T) {
Signature: plugins.SignatureStatusInternal,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -240,6 +241,7 @@ func TestLoader_Load(t *testing.T) {
SignatureOrg: "Grafana Labs",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -292,6 +294,7 @@ func TestLoader_Load(t *testing.T) {
Signature: "unsigned",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -357,6 +360,7 @@ func TestLoader_Load(t *testing.T) {
Signature: plugins.SignatureStatusUnsigned,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -479,6 +483,7 @@ func TestLoader_Load(t *testing.T) {
BaseURL: "public/plugins/test-app",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -571,6 +576,7 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
},
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
}
@@ -688,6 +694,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
SignatureOrg: "Will Browne",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
pluginErrors: map[string]*plugins.Error{
@@ -822,6 +829,7 @@ func TestLoader_Load_RBACReady(t *testing.T) {
BaseURL: "public/plugins/test-app",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -906,6 +914,7 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) {
Module: "public/plugins/test-datasource/module.js",
BaseURL: "public/plugins/test-datasource",
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
}
@@ -1010,6 +1019,7 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
BaseURL: "public/plugins/test-app",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
}
@@ -1118,6 +1128,7 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
BaseURL: "public/plugins/test-app",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
}
@@ -1295,6 +1306,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
Class: plugins.ClassExternal,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
}
child := &plugins.Plugin{
@@ -1354,6 +1366,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
Class: plugins.ClassExternal,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
}
parent.Children = []*plugins.Plugin{child}
@@ -1520,6 +1533,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
Class: plugins.ClassExternal,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
}
child := &plugins.Plugin{
@@ -1587,6 +1601,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
Class: plugins.ClassExternal,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
}
parent.Children = []*plugins.Plugin{child}
@@ -1,90 +0,0 @@
package pluginassets
import (
"context"
"github.com/Masterminds/semver/v3"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
const (
CreatePluginVersionCfgKey = "create_plugin_version"
CreatePluginVersionScriptSupportEnabled = "4.15.0"
)
var (
scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled)
)
func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service, store pluginstore.Store) *Service {
return &Service{
cfg: cfg,
cdn: cdn,
store: store,
log: log.New("pluginassets"),
}
}
type Service struct {
cfg *config.PluginManagementCfg
cdn *pluginscdn.Service
store pluginstore.Store
log log.Logger
}
// LoadingStrategy calculates the loading strategy for a plugin.
// If a plugin has plugin setting `create_plugin_version` >= 4.15.0, set loadingStrategy to "script".
// If a plugin is not loaded via the CDN and is not Angular, set loadingStrategy to "script".
// Otherwise, set loadingStrategy to "fetch".
func (s *Service) LoadingStrategy(_ context.Context, p pluginstore.Plugin) plugins.LoadingStrategy {
if pCfg, ok := s.cfg.PluginSettings[p.ID]; ok {
if s.compatibleCreatePluginVersion(pCfg) {
return plugins.LoadingStrategyScript
}
}
// If the plugin has a parent
if p.Parent != nil {
// Check the parent's create_plugin_version setting
if pCfg, ok := s.cfg.PluginSettings[p.Parent.ID]; ok {
if s.compatibleCreatePluginVersion(pCfg) {
return plugins.LoadingStrategyScript
}
}
// Since the parent plugin is not explicitly configured as script loading compatible,
// If the plugin is either loaded from the CDN (via its parent) or contains Angular, we should use fetch
if s.cdnEnabled(p.Parent.ID, p.FS) || p.Angular.Detected {
return plugins.LoadingStrategyFetch
}
}
if !s.cdnEnabled(p.ID, p.FS) && !p.Angular.Detected {
return plugins.LoadingStrategyScript
}
return plugins.LoadingStrategyFetch
}
func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool {
if cpv, ok := ps[CreatePluginVersionCfgKey]; ok {
createPluginVer, err := semver.NewVersion(cpv)
if err != nil {
s.log.Warn("Failed to parse create plugin version setting as semver", "version", cpv, "error", err)
} else {
if !createPluginVer.LessThan(scriptLoadingMinSupportedVersion) {
return true
}
}
}
return false
}
func (s *Service) cdnEnabled(pluginID string, fs plugins.FS) bool {
return s.cdn.PluginSupported(pluginID) || fs.Type().CDN()
}
@@ -1,220 +0,0 @@
package pluginassets
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
func TestService_Calculate(t *testing.T) {
const pluginID = "grafana-test-datasource"
const (
incompatVersion = "4.14.0"
compatVersion = CreatePluginVersionScriptSupportEnabled
futureVersion = "5.0.0"
)
tcs := []struct {
name string
pluginSettings config.PluginSettings
plugin pluginstore.Plugin
expected plugins.LoadingStrategy
}{
{
name: "Expected LoadingStrategyScript when create-plugin version is compatible and plugin is not angular",
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: compatVersion,
}),
plugin: newPlugin(pluginID, withAngular(false)),
expected: plugins.LoadingStrategyScript,
},
{
name: "Expected LoadingStrategyScript when parent create-plugin version is compatible and plugin is not angular",
pluginSettings: newPluginSettings("parent-datasource", map[string]string{
CreatePluginVersionCfgKey: compatVersion,
}),
plugin: newPlugin(pluginID, withAngular(false), func(p pluginstore.Plugin) pluginstore.Plugin {
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
return p
}),
expected: plugins.LoadingStrategyScript,
},
{
name: "Expected LoadingStrategyScript when create-plugin version is future compatible and plugin is not angular",
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: futureVersion,
}),
plugin: newPlugin(pluginID, withAngular(false), withFS(plugins.NewFakeFS())),
expected: plugins.LoadingStrategyScript,
},
{
name: "Expected LoadingStrategyScript when create-plugin version is not provided, plugin is not angular and is not configured as CDN enabled",
pluginSettings: newPluginSettings(pluginID, map[string]string{
// NOTE: cdn key is not set
}),
plugin: newPlugin(pluginID, withAngular(false), withFS(plugins.NewFakeFS())),
expected: plugins.LoadingStrategyScript,
},
{
name: "Expected LoadingStrategyScript when create-plugin version is not compatible, plugin is not angular, is not configured as CDN enabled and does not have a CDN fs",
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: incompatVersion,
// NOTE: cdn key is not set
}),
plugin: newPlugin(pluginID, withAngular(false), withClass(plugins.ClassExternal), withFS(plugins.NewFakeFS())),
expected: plugins.LoadingStrategyScript,
},
{
name: "Expected LoadingStrategyFetch when parent create-plugin version is not set, is configured as CDN enabled and plugin is not angular",
pluginSettings: config.PluginSettings{
"parent-datasource": {
"cdn": "true",
},
},
plugin: newPlugin(pluginID, withAngular(false), func(p pluginstore.Plugin) pluginstore.Plugin {
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
return p
}),
expected: plugins.LoadingStrategyFetch,
},
{
name: "Expected LoadingStrategyFetch when parent create-plugin version is not set, is configured as CDN enabled and plugin is angular",
pluginSettings: config.PluginSettings{
"parent-datasource": {
"cdn": "true",
},
},
plugin: newPlugin(pluginID, withAngular(true), func(p pluginstore.Plugin) pluginstore.Plugin {
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
return p
}),
expected: plugins.LoadingStrategyFetch,
},
{
name: "Expected LoadingStrategyFetch when parent create-plugin version is not set, is not configured as CDN enabled and plugin is angular",
pluginSettings: config.PluginSettings{},
plugin: newPlugin(pluginID, withAngular(true), withFS(plugins.NewFakeFS()), func(p pluginstore.Plugin) pluginstore.Plugin {
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
return p
}),
expected: plugins.LoadingStrategyFetch,
},
{
name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular, and plugin is configured as CDN enabled",
pluginSettings: newPluginSettings(pluginID, map[string]string{
"cdn": "true",
CreatePluginVersionCfgKey: incompatVersion,
}),
plugin: newPlugin(pluginID, withAngular(false), withClass(plugins.ClassExternal)),
expected: plugins.LoadingStrategyFetch,
},
{
name: "Expected LoadingStrategyFetch when create-plugin version is not compatible and plugin is angular",
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: incompatVersion,
}),
plugin: newPlugin(pluginID, withAngular(true), withFS(plugins.NewFakeFS())),
expected: plugins.LoadingStrategyFetch,
},
{
name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular and plugin is configured as CDN enabled",
pluginSettings: newPluginSettings(pluginID, map[string]string{
"cdn": "true",
CreatePluginVersionCfgKey: incompatVersion,
}),
plugin: newPlugin(pluginID, withAngular(false)),
expected: plugins.LoadingStrategyFetch,
},
{
name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular and has a CDN fs",
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: incompatVersion,
}),
plugin: newPlugin(pluginID, withAngular(false), withFS(
&pluginfakes.FakePluginFS{
TypeFunc: func() plugins.FSType {
return plugins.FSTypeCDN
},
},
)),
expected: plugins.LoadingStrategyFetch,
},
{
name: "Expected LoadingStrategyScript when plugin setting create-plugin version is badly formatted, plugin is not configured as CDN enabled and does not have a CDN fs",
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: "invalidSemver",
}),
plugin: newPlugin(pluginID, withAngular(false), withFS(plugins.NewFakeFS())),
expected: plugins.LoadingStrategyScript,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
s := &Service{
cfg: newCfg(tc.pluginSettings),
cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{
PluginsCDNURLTemplate: "http://cdn.example.com", // required for cdn.PluginSupported check
PluginSettings: tc.pluginSettings,
}),
log: log.NewNopLogger(),
}
got := s.LoadingStrategy(context.Background(), tc.plugin)
assert.Equal(t, tc.expected, got, "unexpected loading strategy")
})
}
}
func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Plugin) pluginstore.Plugin {
p := pluginstore.Plugin{
JSONData: plugins.JSONData{
ID: pluginID,
},
}
for _, cb := range cbs {
p = cb(p)
}
return p
}
func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin {
return func(p pluginstore.Plugin) pluginstore.Plugin {
p.FS = fs
return p
}
}
func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin {
return func(p pluginstore.Plugin) pluginstore.Plugin {
p.Angular = plugins.AngularMeta{Detected: angular}
return p
}
}
func withClass(class plugins.Class) func(p pluginstore.Plugin) pluginstore.Plugin {
return func(p pluginstore.Plugin) pluginstore.Plugin {
p.Class = class
return p
}
}
func newCfg(ps config.PluginSettings) *config.PluginManagementCfg {
return &config.PluginManagementCfg{
PluginSettings: ps,
}
}
func newPluginSettings(pluginID string, kv map[string]string) config.PluginSettings {
return config.PluginSettings{
pluginID: kv,
}
}
@@ -46,7 +46,6 @@ import (
"github.com/grafana/grafana/pkg/services/pluginsintegration/loader"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
@@ -131,7 +130,6 @@ var WireSet = wire.NewSet(
plugincontext.ProvideBaseService,
wire.Bind(new(plugincontext.BasePluginContextProvider), new(*plugincontext.BaseProvider)),
plugininstaller.ProvideService,
pluginassets.ProvideService,
pluginchecker.ProvidePreinstall,
wire.Bind(new(pluginchecker.Preinstall), new(*pluginchecker.PreinstallImpl)),
advisor.ProvideService,
@@ -30,9 +30,10 @@ type Plugin struct {
Error *plugins.Error
// SystemJS fields
Module string
BaseURL string
ModuleHash string
Module string
LoadingStrategy plugins.LoadingStrategy
BaseURL string
ModuleHash string
Angular plugins.AngularMeta
@@ -77,6 +78,7 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin {
SignatureOrg: p.SignatureOrg,
Error: p.Error,
Module: p.Module,
LoadingStrategy: p.LoadingStrategy,
BaseURL: p.BaseURL,
ExternalService: p.ExternalService,
Angular: p.Angular,