Plugins: Introduce LoadingStrategy for frontend loading logic (#92392)

* do it all

* feat(plugins): move loadingStrategy to ds pluginMeta and add to plugin settings endpoint

* support child plugins and update tests

* use relative path for nested plugins

* feat(plugins): support nested plugins in the plugin loader cache by extracting pluginId from path

* feat(grafana-data): add plugin loading strategy to plugin meta and export

* feat(plugins): pass down loadingStrategy to fe plugin loader

* refactor(plugins): make PluginLoadingStrategy an enum

* feat(plugins): add the loading strategy to the fe plugin loader cache

* feat(plugins): load fe plugin js assets as script tags based on be loadingStrategy

* add more tests

* feat(plugins): add loading strategy to plugin preloader

* feat(plugins): make loadingStrategy a maybe and provide fetch fallback

* test(alerting): update config.apps mocks to include loadingStrategy

* fix format

---------

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
This commit is contained in:
Will Browne
2024-09-09 10:38:35 +01:00
committed by GitHub
co-authored by Jack Westbrook
parent d61530941a
commit 2c47d246fc
24 changed files with 724 additions and 119 deletions
@@ -0,0 +1,81 @@
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/pluginscdn"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/setting"
)
const (
CreatePluginVersionCfgKey = "create_plugin_version"
CreatePluginVersionScriptSupportEnabled = "4.15.0"
)
var (
scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled)
)
func ProvideService(cfg *setting.Cfg, cdn *pluginscdn.Service) *Service {
return &Service{
cfg: cfg,
cdn: cdn,
log: log.New("pluginassets"),
}
}
type Service struct {
cfg *setting.Cfg
cdn *pluginscdn.Service
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, check the parent's create_plugin_version setting
if p.Parent != nil {
if pCfg, ok := s.cfg.PluginSettings[p.Parent.ID]; ok {
if s.compatibleCreatePluginVersion(pCfg) {
return plugins.LoadingStrategyScript
}
}
}
if !s.cndEnabled(p) && !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) cndEnabled(p pluginstore.Plugin) bool {
return s.cdn.PluginSupported(p.ID) || p.Class == plugins.ClassCDN
}
@@ -0,0 +1,168 @@
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/pluginscdn"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/setting"
)
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 setting.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, 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, 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, false),
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, false),
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 the CDN class",
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: incompatVersion,
// NOTE: cdn key is not set
}),
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
p.Class = plugins.ClassExternal
return p
}),
expected: plugins.LoadingStrategyScript,
},
{
name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular, is configured as CDN enabled and does not have the CDN class",
pluginSettings: newPluginSettings(pluginID, map[string]string{
"cdn": "true",
CreatePluginVersionCfgKey: incompatVersion,
}),
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
p.Class = plugins.ClassExternal
return p
}),
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, true),
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, false),
expected: plugins.LoadingStrategyFetch,
},
{
name: "Expected LoadingStrategyFetch when create-plugin version is not compatible, plugin is not angular and has the CDN class",
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: incompatVersion,
}),
plugin: newPlugin(pluginID, false, func(p pluginstore.Plugin) pluginstore.Plugin {
p.Class = plugins.ClassCDN
return p
}),
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 the CDN class",
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: "invalidSemver",
}),
plugin: newPlugin(pluginID, false),
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, angular bool, cbs ...func(p pluginstore.Plugin) pluginstore.Plugin) pluginstore.Plugin {
p := pluginstore.Plugin{
JSONData: plugins.JSONData{
ID: pluginID,
},
Angular: plugins.AngularMeta{Detected: angular},
}
for _, cb := range cbs {
p = cb(p)
}
return p
}
func newCfg(ps setting.PluginSettings) *setting.Cfg {
return &setting.Cfg{
PluginSettings: ps,
}
}
func newPluginSettings(pluginID string, kv map[string]string) setting.PluginSettings {
return setting.PluginSettings{
pluginID: kv,
}
}