Plugins: Remove `pluginAssetProvider` feature toggle (#112046)

* remove toggle

* fix lint issue
This commit is contained in:
Will Browne 2025-10-10 10:35:22 +01:00 committed by GitHub
parent 8d17c22006
commit 69f788ad55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 108 additions and 776 deletions

View File

@ -1113,11 +1113,6 @@ export interface FeatureToggles {
*/
alertingNotificationHistory?: boolean;
/**
* Allows decoupled core plugins to load from the Grafana CDN
* @default false
*/
pluginAssetProvider?: boolean;
/**
* Enable dual reader for unified storage search
*/
unifiedStorageSearchDualReaderEnabled?: boolean;

View File

@ -34,7 +34,6 @@ type Features struct {
// Needed only until Tempo Alerting / metrics TraceQL is stable
// https://github.com/grafana/grafana/issues/106888
TempoAlertingEnabled bool
PluginAssetProvider bool
}
// NewPluginManagementCfg returns a new PluginManagementCfg.

View File

@ -698,3 +698,38 @@ func (p *AssetProvider) AssetPath(plugin pluginassets.PluginInfo, assetPath ...s
}
return "", nil
}
type FakeErrorTracker struct {
RecordFunc func(ctx context.Context, err *plugins.Error)
ClearFunc func(ctx context.Context, pluginID string)
ErrorsFunc func(ctx context.Context) []*plugins.Error
}
func NewFakeErrorTracker() *FakeErrorTracker {
return &FakeErrorTracker{}
}
func (t *FakeErrorTracker) Record(ctx context.Context, err *plugins.Error) {
if t.RecordFunc != nil {
t.RecordFunc(ctx, err)
return
}
}
func (t *FakeErrorTracker) Clear(ctx context.Context, pluginID string) {
if t.ClearFunc != nil {
t.ClearFunc(ctx, pluginID)
return
}
}
func (t *FakeErrorTracker) Errors(ctx context.Context) []*plugins.Error {
if t.ErrorsFunc != nil {
return t.ErrorsFunc(ctx)
}
return nil
}
func (t *FakeErrorTracker) Error(ctx context.Context, pluginID string) *plugins.Error {
return &plugins.Error{}
}

View File

@ -1,137 +0,0 @@
package assetpath
import (
"fmt"
"net/url"
"path"
"path/filepath"
"strings"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/pluginassets"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
)
// Service provides methods for constructing asset paths for plugins.
// It supports core plugins, external plugins stored on the local filesystem, and external plugins stored
// on the plugins CDN, and it will switch to the correct implementation depending on the plugin and the config.
type Service struct {
cdn *pluginscdn.Service
cfg *config.PluginManagementCfg
assetProvider pluginassets.Provider
}
func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service, assetProvider pluginassets.Provider) *Service {
return &Service{cfg: cfg, cdn: cdn, assetProvider: assetProvider}
}
func DefaultService(cfg *config.PluginManagementCfg) *Service {
return &Service{cfg: cfg, cdn: pluginscdn.ProvideService(cfg), assetProvider: fakes.NewFakeAssetProvider()}
}
// Base returns the base path for the specified plugin.
func (s *Service) Base(n pluginassets.PluginInfo) (string, error) {
if s.cfg.Features.PluginAssetProvider {
return s.assetProvider.AssetPath(n)
}
if n.Class == plugins.ClassCDN {
return n.FS.Base(), nil
}
if s.cdn.PluginSupported(n.JsonData.ID) {
return s.cdn.AssetURL(n.JsonData.ID, n.JsonData.Info.Version, "")
}
if n.Parent != nil {
relPath, err := n.Parent.FS.Rel(n.FS.Base())
if err != nil {
return "", err
}
if s.cdn.PluginSupported(n.Parent.JsonData.ID) {
return s.cdn.AssetURL(n.Parent.JsonData.ID, n.Parent.JsonData.Info.Version, relPath)
}
}
return path.Join("public/plugins", n.JsonData.ID), nil
}
// Module returns the module.js path for the specified plugin.
func (s *Service) Module(n pluginassets.PluginInfo) (string, error) {
if s.cfg.Features.PluginAssetProvider {
return s.assetProvider.Module(n)
}
if n.Class == plugins.ClassCore && filepath.Base(n.FS.Base()) != "dist" {
return path.Join("core:plugin", filepath.Base(n.FS.Base())), nil
}
return s.RelativeURL(n, "module.js")
}
// RelativeURL returns the relative URL for an arbitrary plugin asset.
func (s *Service) RelativeURL(n pluginassets.PluginInfo, pathStr string) (string, error) {
if s.cfg.Features.PluginAssetProvider {
return s.assetProvider.AssetPath(n, pathStr)
}
if n.Class == plugins.ClassCDN {
return pluginscdn.JoinPath(n.FS.Base(), pathStr)
}
if s.cdn.PluginSupported(n.JsonData.ID) {
return s.cdn.NewCDNURLConstructor(n.JsonData.ID, n.JsonData.Info.Version).StringPath(pathStr)
}
if n.Parent != nil {
if s.cdn.PluginSupported(n.Parent.JsonData.ID) {
relPath, err := n.Parent.FS.Rel(n.FS.Base())
if err != nil {
return "", err
}
return s.cdn.AssetURL(n.Parent.JsonData.ID, n.Parent.JsonData.Info.Version, path.Join(relPath, pathStr))
}
}
// Local
u, err := url.Parse(pathStr)
if err != nil {
return "", fmt.Errorf("url parse: %w", err)
}
if u.IsAbs() {
return pathStr, nil
}
baseURL, err := s.Base(n)
if err != nil {
return "", err
}
// has already been prefixed with base path
if strings.HasPrefix(pathStr, baseURL) {
return pathStr, nil
}
return path.Join(baseURL, pathStr), nil
}
// DefaultLogoPath returns the default logo path for the specified plugin type.
func (s *Service) DefaultLogoPath(pluginType plugins.Type) string {
return path.Join("public/img", fmt.Sprintf("icn-%s.svg", string(pluginType)))
}
func (s *Service) GetTranslations(n pluginassets.PluginInfo) (map[string]string, error) {
pathToTranslations, err := s.RelativeURL(n, "locales")
if err != nil {
return nil, fmt.Errorf("get locales: %w", err)
}
// loop through all the languages specified in the plugin.json and add them to the list
translations := map[string]string{}
for _, language := range n.JsonData.Languages {
file := fmt.Sprintf("%s.json", n.JsonData.ID)
translations[language], err = url.JoinPath(pathToTranslations, language, file)
if err != nil {
return nil, fmt.Errorf("join path: %w", err)
}
}
return translations, nil
}

View File

@ -1,430 +0,0 @@
package assetpath
import (
"net/url"
"path"
"strings"
"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/fakes"
"github.com/grafana/grafana/pkg/plugins/pluginassets"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
)
func pluginFS(basePath string) *fakes.FakePluginFS {
return fakes.NewFakePluginFS(basePath)
}
func TestService(t *testing.T) {
for _, tc := range []struct {
name string
cdnBaseURL string
}{
{
name: "Simple",
cdnBaseURL: "https://cdn.example.com",
},
{
name: "Not root",
cdnBaseURL: "https://cdn.example.com/plugins",
},
{
name: "End slashes",
cdnBaseURL: "https://cdn.example.com/plugins////",
},
} {
t.Run(tc.name, func(t *testing.T) {
cfg := &config.PluginManagementCfg{
PluginsCDNURLTemplate: tc.cdnBaseURL,
PluginSettings: map[string]map[string]string{
"one": {"cdn": "true"},
"two": {},
},
}
svc := ProvideService(cfg, pluginscdn.ProvideService(cfg), fakes.NewFakeAssetProvider())
tableOldFS := fakes.NewFakePluginFS("/grafana/public/app/plugins/panel/table-old")
jsonData := map[string]plugins.JSONData{
"table-old": {ID: "table-old", Info: plugins.Info{Version: "1.0.0"}},
"one": {ID: "one", Info: plugins.Info{Version: "1.0.0"}},
"two": {ID: "two", Info: plugins.Info{Version: "2.0.0"}},
}
t.Run("CDN Base URL", func(t *testing.T) {
base, err := svc.cdn.BaseURL()
require.NoError(t, err)
require.Equal(t, tc.cdnBaseURL, base)
})
t.Run("Base", func(t *testing.T) {
base, err := svc.Base(pluginassets.NewPluginInfo(jsonData["one"], plugins.ClassExternal, pluginFS("one"), nil))
require.NoError(t, err)
oneCDNURL, err := url.JoinPath(tc.cdnBaseURL, "/one/1.0.0/public/plugins/one")
require.NoError(t, err)
require.Equal(t, oneCDNURL, base)
base, err = svc.Base(pluginassets.NewPluginInfo(jsonData["one"], plugins.ClassCDN, pluginFS(oneCDNURL), nil))
require.NoError(t, err)
require.Equal(t, oneCDNURL, base)
base, err = svc.Base(pluginassets.NewPluginInfo(jsonData["two"], plugins.ClassExternal, pluginFS("two"), nil))
require.NoError(t, err)
require.Equal(t, "public/plugins/two", base)
base, err = svc.Base(pluginassets.NewPluginInfo(jsonData["table-old"], plugins.ClassCore, tableOldFS, nil))
require.NoError(t, err)
require.Equal(t, "public/plugins/table-old", base)
parentFS := pluginFS(oneCDNURL)
parentFS.RelFunc = func(_ string) (string, error) {
return "child-plugins/two", nil
}
parent := pluginassets.NewPluginInfo(jsonData["one"], plugins.ClassExternal, parentFS, nil)
child := pluginassets.NewPluginInfo(jsonData["two"], plugins.ClassExternal, fakes.NewFakePluginFS(""), &parent)
base, err = svc.Base(child)
require.NoError(t, err)
childBase, err := url.JoinPath(oneCDNURL, "child-plugins/two")
require.NoError(t, err)
require.Equal(t, childBase, base)
})
t.Run("Module", func(t *testing.T) {
module, err := svc.Module(pluginassets.NewPluginInfo(jsonData["one"], plugins.ClassExternal, pluginFS("one"), nil))
require.NoError(t, err)
oneCDNURL, err := url.JoinPath(tc.cdnBaseURL, "/one/1.0.0/public/plugins/one")
require.NoError(t, err)
oneCDNModuleURL, err := url.JoinPath(oneCDNURL, "module.js")
require.NoError(t, err)
require.Equal(t, oneCDNModuleURL, module)
fs := pluginFS("one")
module, err = svc.Module(pluginassets.NewPluginInfo(jsonData["one"], plugins.ClassCDN, fs, nil))
require.NoError(t, err)
require.Equal(t, path.Join(fs.Base(), "module.js"), module)
module, err = svc.Module(pluginassets.NewPluginInfo(jsonData["two"], plugins.ClassExternal, pluginFS("two"), nil))
require.NoError(t, err)
require.Equal(t, "public/plugins/two/module.js", module)
module, err = svc.Module(pluginassets.NewPluginInfo(jsonData["table-old"], plugins.ClassCore, tableOldFS, nil))
require.NoError(t, err)
require.Equal(t, "core:plugin/table-old", module)
parentFS := pluginFS(oneCDNURL)
parentFS.RelFunc = func(_ string) (string, error) {
return "child-plugins/two", nil
}
parent := pluginassets.NewPluginInfo(jsonData["one"], plugins.ClassExternal, parentFS, nil)
child := pluginassets.NewPluginInfo(jsonData["two"], plugins.ClassExternal, fakes.NewFakePluginFS(""), &parent)
module, err = svc.Module(child)
require.NoError(t, err)
childModule, err := url.JoinPath(oneCDNURL, "child-plugins/two/module.js")
require.NoError(t, err)
require.Equal(t, childModule, module)
})
t.Run("RelativeURL", func(t *testing.T) {
pluginsMap := map[string]*plugins.Plugin{
"one": {
JSONData: plugins.JSONData{ID: "one", Info: plugins.Info{Version: "1.0.0"}},
},
"two": {
JSONData: plugins.JSONData{ID: "two", Info: plugins.Info{Version: "2.0.0"}},
},
}
u, err := svc.RelativeURL(pluginassets.NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassExternal, pluginFS("one"), nil), "")
require.NoError(t, err)
// given an empty path, base URL will be returned
baseURL, err := svc.Base(pluginassets.NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassExternal, pluginFS("one"), nil))
require.NoError(t, err)
require.Equal(t, baseURL, u)
u, err = svc.RelativeURL(pluginassets.NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassExternal, pluginFS("one"), nil), "path/to/file.txt")
require.NoError(t, err)
require.Equal(t, strings.TrimRight(tc.cdnBaseURL, "/")+"/one/1.0.0/public/plugins/one/path/to/file.txt", u)
u, err = svc.RelativeURL(pluginassets.NewPluginInfo(pluginsMap["two"].JSONData, plugins.ClassExternal, pluginFS("two"), nil), "path/to/file.txt")
require.NoError(t, err)
require.Equal(t, "public/plugins/two/path/to/file.txt", u)
u, err = svc.RelativeURL(pluginassets.NewPluginInfo(pluginsMap["two"].JSONData, plugins.ClassExternal, pluginFS("two"), nil), "default")
require.NoError(t, err)
require.Equal(t, "public/plugins/two/default", u)
oneCDNURL, err := url.JoinPath(tc.cdnBaseURL, "/one/1.0.0/public/plugins/one")
require.NoError(t, err)
u, err = svc.RelativeURL(pluginassets.NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassCDN, pluginFS(oneCDNURL), nil), "path/to/file.txt")
require.NoError(t, err)
oneCDNRelativeURL, err := url.JoinPath(oneCDNURL, "path/to/file.txt")
require.NoError(t, err)
require.Equal(t, oneCDNRelativeURL, u)
parentFS := pluginFS(oneCDNURL)
parentFS.RelFunc = func(_ string) (string, error) {
return "child-plugins/two", nil
}
parent := pluginassets.NewPluginInfo(jsonData["one"], plugins.ClassExternal, parentFS, nil)
child := pluginassets.NewPluginInfo(jsonData["two"], plugins.ClassExternal, fakes.NewFakePluginFS(""), &parent)
u, err = svc.RelativeURL(child, "path/to/file.txt")
require.NoError(t, err)
oneCDNRelativeURL, err = url.JoinPath(oneCDNURL, "child-plugins/two/path/to/file.txt")
require.NoError(t, err)
require.Equal(t, oneCDNRelativeURL, u)
u, err = svc.RelativeURL(child, "../path/to/file.txt")
require.NoError(t, err)
oneCDNRelativeURL, err = url.JoinPath(oneCDNURL, "child-plugins/path/to/file.txt")
require.NoError(t, err)
require.Equal(t, oneCDNRelativeURL, u)
})
t.Run("GetTranslations", func(t *testing.T) {
pluginInfo := pluginassets.NewPluginInfo(jsonData["one"], plugins.ClassExternal, pluginFS("one"), nil)
pluginInfo.JsonData.Languages = []string{"en-US", "pt-BR"}
translations, err := svc.GetTranslations(pluginInfo)
require.NoError(t, err)
oneCDNURL, err := url.JoinPath(tc.cdnBaseURL, "one", "1.0.0", "public", "plugins", "one")
require.NoError(t, err)
enURL, err := url.JoinPath(oneCDNURL, "locales", "en-US", "one.json")
require.NoError(t, err)
ptBRURL, err := url.JoinPath(oneCDNURL, "locales", "pt-BR", "one.json")
require.NoError(t, err)
require.Equal(t, map[string]string{"en-US": enURL, "pt-BR": ptBRURL}, translations)
})
})
}
}
func TestService_ChildPlugins(t *testing.T) {
type expected struct {
module string
baseURL string
relURL string
}
tcs := []struct {
name string
cfg *config.PluginManagementCfg
pluginInfo func() pluginassets.PluginInfo
expected expected
}{
{
name: "Local FS external plugin",
cfg: &config.PluginManagementCfg{},
pluginInfo: func() pluginassets.PluginInfo {
return pluginassets.NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassExternal, plugins.NewLocalFS("/plugins/parent"), nil)
},
expected: expected{
module: "public/plugins/parent/module.js",
baseURL: "public/plugins/parent",
relURL: "public/plugins/parent/path/to/file.txt",
},
},
{
name: "Local FS external plugin with child",
cfg: &config.PluginManagementCfg{},
pluginInfo: func() pluginassets.PluginInfo {
parentInfo := pluginassets.NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassExternal, plugins.NewLocalFS("/plugins/parent"), nil)
childInfo := pluginassets.NewPluginInfo(plugins.JSONData{ID: "child", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassExternal, plugins.NewLocalFS("/plugins/parent/child"), &parentInfo)
return childInfo
},
expected: expected{
module: "public/plugins/child/module.js",
baseURL: "public/plugins/child",
relURL: "public/plugins/child/path/to/file.txt",
},
},
{
name: "Local FS core plugin",
cfg: &config.PluginManagementCfg{},
pluginInfo: func() pluginassets.PluginInfo {
return pluginassets.NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassCore, plugins.NewLocalFS("/plugins/parent"), nil)
},
expected: expected{
module: "core:plugin/parent",
baseURL: "public/plugins/parent",
relURL: "public/plugins/parent/path/to/file.txt",
},
},
{
name: "Externally-built Local FS core plugin",
cfg: &config.PluginManagementCfg{},
pluginInfo: func() pluginassets.PluginInfo {
return pluginassets.NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassCore, plugins.NewLocalFS("/plugins/parent/dist"), nil)
},
expected: expected{
module: "public/plugins/parent/module.js",
baseURL: "public/plugins/parent",
relURL: "public/plugins/parent/path/to/file.txt",
},
},
{
name: "CDN Class plugin",
cfg: &config.PluginManagementCfg{
PluginsCDNURLTemplate: "https://cdn.example.com",
},
pluginInfo: func() pluginassets.PluginInfo {
return pluginassets.NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassCDN, pluginFS("https://cdn.example.com/plugins/parent"), nil)
},
expected: expected{
module: "https://cdn.example.com/plugins/parent/module.js",
baseURL: "https://cdn.example.com/plugins/parent",
relURL: "https://cdn.example.com/plugins/parent/path/to/file.txt",
},
},
{
name: "CDN Class plugin with child",
cfg: &config.PluginManagementCfg{
PluginsCDNURLTemplate: "https://cdn.example.com",
},
pluginInfo: func() pluginassets.PluginInfo {
// Note: fake plugin FS is the most convenient way to mock the plugin FS for CDN plugins
parentInfo := pluginassets.NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassCDN, pluginFS("https://cdn.example.com/parent"), nil)
childInfo := pluginassets.NewPluginInfo(plugins.JSONData{ID: "child", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassCDN, pluginFS("https://cdn.example.com/parent/some/other/dir/child"), &parentInfo)
return childInfo
},
expected: expected{
module: "https://cdn.example.com/parent/some/other/dir/child/module.js",
baseURL: "https://cdn.example.com/parent/some/other/dir/child",
relURL: "https://cdn.example.com/parent/some/other/dir/child/path/to/file.txt",
},
},
{
name: "CDN supported plugin",
cfg: &config.PluginManagementCfg{
PluginsCDNURLTemplate: "https://cdn.example.com",
PluginSettings: map[string]map[string]string{
"parent": {"cdn": "true"},
},
},
pluginInfo: func() pluginassets.PluginInfo {
return pluginassets.NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassExternal, plugins.NewLocalFS("/plugins/parent"), nil)
},
expected: expected{
module: "https://cdn.example.com/parent/1.0.0/public/plugins/parent/module.js",
baseURL: "https://cdn.example.com/parent/1.0.0/public/plugins/parent",
relURL: "https://cdn.example.com/parent/1.0.0/public/plugins/parent/path/to/file.txt",
},
},
{
name: "CDN supported plugin with child",
cfg: &config.PluginManagementCfg{
PluginsCDNURLTemplate: "https://cdn.example.com",
PluginSettings: map[string]map[string]string{
"parent": {"cdn": "true"},
},
},
pluginInfo: func() pluginassets.PluginInfo {
parentInfo := pluginassets.NewPluginInfo(plugins.JSONData{ID: "parent", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassExternal, plugins.NewLocalFS("/plugins/parent"), nil)
childInfo := pluginassets.NewPluginInfo(plugins.JSONData{ID: "child", Info: plugins.Info{Version: "1.0.0"}}, plugins.ClassExternal, plugins.NewLocalFS("/plugins/parent/child"), &parentInfo)
return childInfo
},
expected: expected{
module: "https://cdn.example.com/parent/1.0.0/public/plugins/parent/child/module.js",
baseURL: "https://cdn.example.com/parent/1.0.0/public/plugins/parent/child",
relURL: "https://cdn.example.com/parent/1.0.0/public/plugins/parent/child/path/to/file.txt",
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
svc := ProvideService(tc.cfg, pluginscdn.ProvideService(tc.cfg), fakes.NewFakeAssetProvider())
module, err := svc.Module(tc.pluginInfo())
require.NoError(t, err)
require.Equal(t, tc.expected.module, module)
baseURL, err := svc.Base(tc.pluginInfo())
require.NoError(t, err)
require.Equal(t, tc.expected.baseURL, baseURL)
relURL, err := svc.RelativeURL(tc.pluginInfo(), "path/to/file.txt")
require.NoError(t, err)
require.Equal(t, tc.expected.relURL, relURL)
})
}
}
func TestService_AssetProviderPrecedence(t *testing.T) {
cfg := &config.PluginManagementCfg{
PluginsCDNURLTemplate: "https://cdn.example.com",
PluginSettings: map[string]map[string]string{
"test-plugin": {"cdn": "true"},
},
Features: config.Features{PluginAssetProvider: true},
}
cdn := pluginscdn.ProvideService(cfg)
pluginInfo := pluginassets.NewPluginInfo(
plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}},
plugins.ClassExternal,
pluginFS("/plugins/test-plugin"),
nil,
)
t.Run("Asset provider enabled takes precedence when feature enabled", func(t *testing.T) {
assetProvider := fakes.NewFakeAssetProvider()
assetProvider.AssetPathFunc = func(n pluginassets.PluginInfo, pathElems ...string) (string, error) {
return "from-asset-provider", nil
}
assetProvider.ModuleFunc = func(n pluginassets.PluginInfo) (string, error) {
return "module-from-asset-provider", nil
}
svc := ProvideService(cfg, cdn, assetProvider)
base, err := svc.Base(pluginInfo)
require.NoError(t, err)
require.Equal(t, "from-asset-provider", base)
module, err := svc.Module(pluginInfo)
require.NoError(t, err)
require.Equal(t, "module-from-asset-provider", module)
relURL, err := svc.RelativeURL(pluginInfo, "path/to/file.txt")
require.NoError(t, err)
require.Equal(t, "from-asset-provider", relURL)
})
t.Run("GetTranslations uses asset provider when feature enabled", func(t *testing.T) {
assetProvider := fakes.NewFakeAssetProvider()
assetProvider.AssetPathFunc = func(n pluginassets.PluginInfo, pathElems ...string) (string, error) {
return path.Join("translation-path", path.Join(pathElems...)), nil
}
pluginWithLangs := pluginassets.NewPluginInfo(
plugins.JSONData{
ID: "multilang-plugin",
Info: plugins.Info{Version: "1.0.0"},
Languages: []string{"en-US", "es-ES", "fr-FR"},
},
plugins.ClassExternal,
pluginFS("/plugins/multilang-plugin"),
nil,
)
svc := ProvideService(cfg, cdn, assetProvider)
translations, err := svc.GetTranslations(pluginWithLangs)
require.NoError(t, err)
require.Len(t, translations, 3)
// All translation paths should come from the asset provider
require.Equal(t, "translation-path/locales/en-US/multilang-plugin.json", translations["en-US"])
require.Equal(t, "translation-path/locales/es-ES/multilang-plugin.json", translations["es-ES"])
require.Equal(t, "translation-path/locales/fr-FR/multilang-plugin.json", translations["fr-FR"])
})
}

View File

@ -6,8 +6,8 @@ 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/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/pluginassets"
)
// Bootstrapper is responsible for the Bootstrap stage of the plugin loader pipeline.
@ -44,7 +44,7 @@ type Opts struct {
// New returns a new Bootstrap stage.
func New(cfg *config.PluginManagementCfg, opts Opts) *Bootstrap {
if opts.ConstructFunc == nil {
opts.ConstructFunc = DefaultConstructFunc(cfg, signature.DefaultCalculator(cfg), assetpath.DefaultService(cfg))
opts.ConstructFunc = DefaultConstructFunc(cfg, signature.DefaultCalculator(cfg), pluginassets.NewLocalProvider())
}
if opts.DecorateFuncs == nil {

View File

@ -2,11 +2,12 @@ package bootstrap
import (
"fmt"
"net/url"
"path"
"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/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/pluginassets"
)
@ -17,13 +18,13 @@ type pluginFactoryFunc func(p *plugins.FoundBundle, pluginClass plugins.Class, s
// It creates the plugin using plugin information found during the Discovery stage and makes use of the assetPath
// service to set the plugin's BaseURL, Module, Logos and Screenshots fields.
type DefaultPluginFactory struct {
assetPath *assetpath.Service
features *config.Features
assetProvider pluginassets.Provider
features *config.Features
}
// NewDefaultPluginFactory returns a new DefaultPluginFactory.
func NewDefaultPluginFactory(features *config.Features, assetPath *assetpath.Service) *DefaultPluginFactory {
return &DefaultPluginFactory{assetPath: assetPath, features: features}
func NewDefaultPluginFactory(features *config.Features, assetProvider pluginassets.Provider) *DefaultPluginFactory {
return &DefaultPluginFactory{assetProvider: assetProvider, features: features}
}
func (f *DefaultPluginFactory) createPlugin(bundle *plugins.FoundBundle, class plugins.Class,
@ -54,11 +55,11 @@ func (f *DefaultPluginFactory) createPlugin(bundle *plugins.FoundBundle, class p
func (f *DefaultPluginFactory) newPlugin(p plugins.FoundPlugin, class plugins.Class, sig plugins.Signature,
info pluginassets.PluginInfo) (*plugins.Plugin, error) {
baseURL, err := f.assetPath.Base(info)
baseURL, err := f.assetProvider.AssetPath(info)
if err != nil {
return nil, fmt.Errorf("base url: %w", err)
}
moduleURL, err := f.assetPath.Module(info)
moduleURL, err := f.assetProvider.Module(info)
if err != nil {
return nil, fmt.Errorf("module url: %w", err)
}
@ -74,33 +75,33 @@ func (f *DefaultPluginFactory) newPlugin(p plugins.FoundPlugin, class plugins.Cl
}
plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID)))
if err = setImages(plugin, f.assetPath, info); err != nil {
if err = setImages(plugin, f.assetProvider, info); err != nil {
return nil, err
}
if err := setTranslations(plugin, f.assetPath, info); err != nil {
if err := setTranslations(plugin, f.assetProvider, info); err != nil {
return nil, err
}
return plugin, nil
}
func setImages(p *plugins.Plugin, assetPath *assetpath.Service, info pluginassets.PluginInfo) error {
func setImages(p *plugins.Plugin, assetProvider pluginassets.Provider, info pluginassets.PluginInfo) error {
var err error
for _, dst := range []*string{&p.Info.Logos.Small, &p.Info.Logos.Large} {
if len(*dst) == 0 {
*dst = assetPath.DefaultLogoPath(p.Type)
*dst = defaultLogoPath(p.Type)
continue
}
*dst, err = assetPath.RelativeURL(info, *dst)
*dst, err = assetProvider.AssetPath(info, *dst)
if err != nil {
return fmt.Errorf("logo: %w", err)
}
}
for i := 0; i < len(p.Info.Screenshots); i++ {
screenshot := &p.Info.Screenshots[i]
screenshot.Path, err = assetPath.RelativeURL(info, screenshot.Path)
screenshot.Path, err = assetProvider.AssetPath(info, screenshot.Path)
if err != nil {
return fmt.Errorf("screenshot %d relative url: %w", i, err)
}
@ -108,8 +109,8 @@ func setImages(p *plugins.Plugin, assetPath *assetpath.Service, info pluginasset
return nil
}
func setTranslations(p *plugins.Plugin, assetPath *assetpath.Service, info pluginassets.PluginInfo) error {
translations, err := assetPath.GetTranslations(info)
func setTranslations(p *plugins.Plugin, assetProvider pluginassets.Provider, info pluginassets.PluginInfo) error {
translations, err := getTranslations(assetProvider, info)
if err != nil {
return fmt.Errorf("set translations: %w", err)
}
@ -117,3 +118,26 @@ func setTranslations(p *plugins.Plugin, assetPath *assetpath.Service, info plugi
p.Translations = translations
return nil
}
func defaultLogoPath(pluginType plugins.Type) string {
return path.Join("public/img", fmt.Sprintf("icn-%s.svg", string(pluginType)))
}
func getTranslations(assetProvider pluginassets.Provider, n pluginassets.PluginInfo) (map[string]string, error) {
pathToTranslations, err := assetProvider.AssetPath(n, "locales")
if err != nil {
return nil, fmt.Errorf("get locales: %w", err)
}
// loop through all the languages specified in the plugin.json and add them to the list
translations := map[string]string{}
for _, language := range n.JsonData.Languages {
file := fmt.Sprintf("%s.json", n.JsonData.ID)
translations[language], err = url.JoinPath(pathToTranslations, language, file)
if err != nil {
return nil, fmt.Errorf("join path: %w", err)
}
}
return translations, nil
}

View File

@ -9,7 +9,7 @@ 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/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/pluginassets"
)
// DefaultConstructor implements the default ConstructFunc used for the Construct step of the Bootstrap stage.
@ -22,8 +22,8 @@ type DefaultConstructor struct {
}
// DefaultConstructFunc is the default ConstructFunc used for the Construct step of the Bootstrap stage.
func DefaultConstructFunc(cfg *config.PluginManagementCfg, signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) ConstructFunc {
return NewDefaultConstructor(cfg, signatureCalculator, assetPath).Construct
func DefaultConstructFunc(cfg *config.PluginManagementCfg, signatureCalculator plugins.SignatureCalculator, assetProvider pluginassets.Provider) ConstructFunc {
return NewDefaultConstructor(cfg, signatureCalculator, assetProvider).Construct
}
// DefaultDecorateFuncs are the default DecorateFuncs used for the Decorate step of the Bootstrap stage.
@ -37,9 +37,9 @@ func DefaultDecorateFuncs(cfg *config.PluginManagementCfg) []DecorateFunc {
}
// NewDefaultConstructor returns a new DefaultConstructor.
func NewDefaultConstructor(cfg *config.PluginManagementCfg, signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) *DefaultConstructor {
func NewDefaultConstructor(cfg *config.PluginManagementCfg, signatureCalculator plugins.SignatureCalculator, assetProvider pluginassets.Provider) *DefaultConstructor {
return &DefaultConstructor{
pluginFactoryFunc: NewDefaultPluginFactory(&cfg.Features, assetPath).createPlugin,
pluginFactoryFunc: NewDefaultPluginFactory(&cfg.Features, assetProvider).createPlugin,
signatureCalculator: signatureCalculator,
log: log.New("plugins.construct"),
}

View File

@ -11,7 +11,7 @@ var _ Provider = (*LocalProvider)(nil)
type LocalProvider struct{}
func ProvideService() *LocalProvider {
func NewLocalProvider() *LocalProvider {
return &LocalProvider{}
}

View File

@ -40,7 +40,6 @@ import (
provider2 "github.com/grafana/grafana/pkg/plugins/backendplugin/provider"
manager4 "github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/plugins/manager/filestore"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
@ -361,10 +360,8 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
keyRetriever := dynamic.ProvideService(cfg, keystoreService)
keyretrieverService := keyretriever.ProvideService(keyRetriever)
signatureSignature := signature.ProvideService(pluginManagementCfg, keyretrieverService)
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
localProvider := pluginassets.ProvideService()
assetpathService := assetpath.ProvideService(pluginManagementCfg, pluginscdnService, localProvider)
bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, assetpathService)
localProvider := pluginassets.NewLocalProvider()
bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider)
unsignedPluginAuthorizer := signature.ProvideOSSAuthorizer(pluginManagementCfg)
validation := signature.ProvideValidatorService(unsignedPluginAuthorizer)
angularpatternsstoreService := angularpatternsstore.ProvideService(kvStore)
@ -677,6 +674,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
if err != nil {
return nil, err
}
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService)
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
prefService := prefimpl.ProvideService(sqlStore, cfg)
@ -963,10 +961,8 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
keyRetriever := dynamic.ProvideService(cfg, keystoreService)
keyretrieverService := keyretriever.ProvideService(keyRetriever)
signatureSignature := signature.ProvideService(pluginManagementCfg, keyretrieverService)
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
localProvider := pluginassets.ProvideService()
assetpathService := assetpath.ProvideService(pluginManagementCfg, pluginscdnService, localProvider)
bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, assetpathService)
localProvider := pluginassets.NewLocalProvider()
bootstrap := pipeline.ProvideBootstrapStage(pluginManagementCfg, signatureSignature, localProvider)
unsignedPluginAuthorizer := signature.ProvideOSSAuthorizer(pluginManagementCfg)
validation := signature.ProvideValidatorService(unsignedPluginAuthorizer)
angularpatternsstoreService := angularpatternsstore.ProvideService(kvStore)
@ -1281,6 +1277,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
if err != nil {
return nil, err
}
pluginscdnService := pluginscdn.ProvideService(pluginManagementCfg)
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, signatureSignature, pluginstoreService)
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
prefService := prefimpl.ProvideService(sqlStore, cfg)

View File

@ -1929,16 +1929,6 @@ var (
HideFromDocs: true,
Expression: "false",
},
{
Name: "pluginAssetProvider",
Description: "Allows decoupled core plugins to load from the Grafana CDN",
Stage: FeatureStageExperimental,
Owner: grafanaPluginsPlatformSquad,
HideFromAdminPage: true,
HideFromDocs: true,
Expression: "false",
RequiresRestart: true,
},
{
Name: "unifiedStorageSearchDualReaderEnabled",
Description: "Enable dual reader for unified storage search",

View File

@ -248,7 +248,6 @@ foldersAppPlatformAPI,experimental,@grafana/grafana-search-navigate-organise,fal
enablePluginImporter,experimental,@grafana/plugins-platform-backend,false,false,true
otelLogsFormatting,experimental,@grafana/observability-logs,false,false,true
alertingNotificationHistory,experimental,@grafana/alerting-squad,false,false,false
pluginAssetProvider,experimental,@grafana/plugins-platform-backend,false,true,false
unifiedStorageSearchDualReaderEnabled,experimental,@grafana/search-and-storage,false,false,false
dashboardDsAdHocFiltering,GA,@grafana/datapro,false,false,true
dashboardLevelTimeMacros,experimental,@grafana/dashboards-squad,false,false,true

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
248 enablePluginImporter experimental @grafana/plugins-platform-backend false false true
249 otelLogsFormatting experimental @grafana/observability-logs false false true
250 alertingNotificationHistory experimental @grafana/alerting-squad false false false
pluginAssetProvider experimental @grafana/plugins-platform-backend false true false
251 unifiedStorageSearchDualReaderEnabled experimental @grafana/search-and-storage false false false
252 dashboardDsAdHocFiltering GA @grafana/datapro false false true
253 dashboardLevelTimeMacros experimental @grafana/dashboards-squad false false true

View File

@ -1002,10 +1002,6 @@ const (
// Enables the notification history feature
FlagAlertingNotificationHistory = "alertingNotificationHistory"
// FlagPluginAssetProvider
// Allows decoupled core plugins to load from the Grafana CDN
FlagPluginAssetProvider = "pluginAssetProvider"
// FlagUnifiedStorageSearchDualReaderEnabled
// Enable dual reader for unified storage search
FlagUnifiedStorageSearchDualReaderEnabled = "unifiedStorageSearchDualReaderEnabled"

View File

@ -2814,7 +2814,8 @@
"metadata": {
"name": "pluginAssetProvider",
"resourceVersion": "1753448760331",
"creationTimestamp": "2025-07-17T15:20:35Z"
"creationTimestamp": "2025-07-17T15:20:35Z",
"deletionTimestamp": "2025-10-03T16:13:14Z"
},
"spec": {
"description": "Allows decoupled core plugins to load from the Grafana CDN",

View File

@ -1,42 +0,0 @@
package loader
import (
"context"
"github.com/grafana/grafana/pkg/plugins"
)
type fakeErrorTracker struct {
RecordFunc func(ctx context.Context, err *plugins.Error)
ClearFunc func(ctx context.Context, pluginID string)
ErrorsFunc func(ctx context.Context) []*plugins.Error
}
func newFakeErrorTracker() *fakeErrorTracker {
return &fakeErrorTracker{}
}
func (t *fakeErrorTracker) Record(ctx context.Context, err *plugins.Error) {
if t.RecordFunc != nil {
t.RecordFunc(ctx, err)
return
}
}
func (t *fakeErrorTracker) Clear(ctx context.Context, pluginID string) {
if t.ClearFunc != nil {
t.ClearFunc(ctx, pluginID)
return
}
}
func (t *fakeErrorTracker) Errors(ctx context.Context) []*plugins.Error {
if t.ErrorsFunc != nil {
return t.ErrorsFunc(ctx)
}
return nil
}
func (t *fakeErrorTracker) Error(ctx context.Context, pluginID string) *plugins.Error {
return &plugins.Error{}
}

View File

@ -20,18 +20,15 @@ import (
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/process"
"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/plugins/pluginassets"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
"github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins"
"github.com/grafana/grafana/pkg/setting"
)
var compareOpts = []cmp.Option{cmpopts.IgnoreFields(plugins.Plugin{}, "client", "log", "mu"), fsComparer}
@ -468,7 +465,8 @@ func TestLoader_Load(t *testing.T) {
procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager()
errTracker := pluginerrs.ProvideErrorTracker()
l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr, errTracker)
l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr, errTracker, pluginassets.NewLocalProvider())
t.Run(tt.name, func(t *testing.T) {
got, err := l.Load(context.Background(), sources.NewLocalSource(tt.class, tt.pluginPaths))
@ -586,91 +584,6 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
})
}
func TestLoader_Load_CustomSource(t *testing.T) {
t.Run("Load a plugin", func(t *testing.T) {
cfg := &config.PluginManagementCfg{
PluginsCDNURLTemplate: "https://cdn.example.com",
PluginSettings: setting.PluginSettings{
"grafana-worldmap-panel": {"cdn": "true"},
},
}
pluginPaths := []string{filepath.Join(testDataDir(t), "cdn")}
expected := []*plugins.Plugin{{
JSONData: plugins.JSONData{
ID: "grafana-worldmap-panel",
Type: plugins.TypePanel,
Name: "Worldmap Panel",
Info: plugins.Info{
Version: "0.3.3",
Links: []plugins.InfoLink{
{Name: "Project site", URL: "https://github.com/grafana/worldmap-panel"},
{Name: "MIT License", URL: "https://github.com/grafana/worldmap-panel/blob/master/LICENSE"},
},
Logos: plugins.Logos{
// Path substitution
Small: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap_logo.svg",
Large: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap_logo.svg",
},
Screenshots: []plugins.Screenshots{
{
Name: "World",
Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-world.png",
},
{
Name: "USA",
Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-usa.png",
},
{
Name: "Light Theme",
Path: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/images/worldmap-light-theme.png",
},
},
},
Dependencies: plugins.Dependencies{
GrafanaVersion: "3.x.x",
Plugins: []plugins.Dependency{},
Extensions: plugins.ExtensionsDependencies{
ExposedComponents: []string{},
},
},
Extensions: plugins.Extensions{
AddedLinks: []plugins.AddedLink{},
AddedComponents: []plugins.AddedComponent{},
AddedFunctions: []plugins.AddedFunction{},
ExposedComponents: []plugins.ExposedComponent{},
ExtensionPoints: []plugins.ExtensionPoint{},
},
},
FS: mustNewStaticFSForTests(t, filepath.Join(testDataDir(t), "cdn/plugin")),
Class: plugins.ClassExternal,
Signature: plugins.SignatureStatusValid,
BaseURL: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel",
Module: "https://cdn.example.com/grafana-worldmap-panel/0.3.3/public/plugins/grafana-worldmap-panel/module.js",
Translations: map[string]string{},
}}
l := newLoader(t, cfg, fakes.NewFakePluginRegistry(), fakes.NewFakeProcessManager(), fakes.NewFakeBackendProcessProvider(), newFakeErrorTracker())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.ClassExternal
},
DiscoverFunc: sources.NewLocalSource(plugins.ClassExternal, pluginPaths).Discover,
DefaultSignatureFunc: func(ctx context.Context) (plugins.Signature, bool) {
return plugins.Signature{
Status: plugins.SignatureStatusValid,
}, true
},
})
require.NoError(t, err)
if !cmp.Equal(got, expected, compareOpts...) {
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...))
}
})
}
func TestLoader_Load_MultiplePlugins(t *testing.T) {
t.Run("Load multiple", func(t *testing.T) {
tests := []struct {
@ -753,7 +666,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
procMgr := fakes.NewFakeProcessManager()
errTracker := pluginerrs.ProvideErrorTracker()
l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr, errTracker)
l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr, errTracker, pluginassets.NewLocalProvider())
t.Run(tt.name, func(t *testing.T) {
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
@ -864,7 +777,7 @@ func TestLoader_Load_RBACReady(t *testing.T) {
reg := fakes.NewFakePluginRegistry()
procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager()
l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr, newFakeErrorTracker())
l := newLoader(t, tt.cfg, reg, procMgr, procPrvdr, fakes.NewFakeErrorTracker(), pluginassets.NewLocalProvider())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
@ -931,7 +844,7 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) {
procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager()
cfg := &config.PluginManagementCfg{GrafanaAppURL: defaultAppURL}
l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeErrorTracker())
l := newLoader(t, cfg, reg, procMgr, procPrvdr, fakes.NewFakeErrorTracker(), pluginassets.NewLocalProvider())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.ClassExternal
@ -1018,7 +931,7 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager()
cfg := &config.PluginManagementCfg{}
l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeErrorTracker())
l := newLoader(t, cfg, reg, procMgr, procPrvdr, fakes.NewFakeErrorTracker(), pluginassets.NewLocalProvider())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.ClassExternal
@ -1118,7 +1031,7 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
}
procMgr := fakes.NewFakeProcessManager()
cfg := &config.PluginManagementCfg{}
l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeErrorTracker())
l := newLoader(t, cfg, reg, procMgr, procPrvdr, fakes.NewFakeErrorTracker(), pluginassets.NewLocalProvider())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.ClassExternal
@ -1318,7 +1231,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
procMgr := fakes.NewFakeProcessManager()
reg := fakes.NewFakePluginRegistry()
cfg := &config.PluginManagementCfg{}
l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeErrorTracker())
l := newLoader(t, cfg, reg, procMgr, procPrvdr, fakes.NewFakeErrorTracker(), pluginassets.NewLocalProvider())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
@ -1519,7 +1432,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
procPrvdr := fakes.NewFakeBackendProcessProvider()
procMgr := fakes.NewFakeProcessManager()
cfg := &config.PluginManagementCfg{}
l := newLoader(t, cfg, reg, procMgr, procPrvdr, newFakeErrorTracker())
l := newLoader(t, cfg, reg, procMgr, procPrvdr, fakes.NewFakeErrorTracker(), pluginassets.NewLocalProvider())
got, err := l.Load(context.Background(), &fakes.FakePluginSource{
PluginClassFunc: func(ctx context.Context) plugins.Class {
return plugins.ClassExternal
@ -1548,23 +1461,21 @@ type loaderDepOpts struct {
}
func newLoader(t *testing.T, cfg *config.PluginManagementCfg, reg registry.Service, proc process.Manager,
backendFactory plugins.BackendFactoryProvider, errTracker pluginerrs.ErrorTracker,
backendFactory plugins.BackendFactoryProvider, errTracker pluginerrs.ErrorTracker, pluginAssetsProvider pluginassets.Provider,
) *Loader {
assets := assetpath.ProvideService(cfg, pluginscdn.ProvideService(cfg), pluginassets.ProvideService())
angularInspector := angularinspector.NewStaticInspector()
terminate, err := pipeline.ProvideTerminationStage(cfg, reg, proc)
require.NoError(t, err)
return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, reg),
pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets),
pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginAssetsProvider),
pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector),
pipeline.ProvideInitializationStage(cfg, reg, backendFactory, proc, &fakes.FakeAuthService{}, fakes.NewFakeRoleRegistry(), fakes.NewFakeActionSetRegistry(), fakes.NewFakePluginEnvProvider(), tracing.InitializeTracerForTest(), provisionedplugins.NewNoop()),
terminate, errTracker)
}
func newLoaderWithOpts(t *testing.T, cfg *config.PluginManagementCfg, opts loaderDepOpts) *Loader {
assets := assetpath.ProvideService(cfg, pluginscdn.ProvideService(cfg), pluginassets.ProvideService())
reg := fakes.NewFakePluginRegistry()
proc := fakes.NewFakeProcessManager()
@ -1588,7 +1499,7 @@ func newLoaderWithOpts(t *testing.T, cfg *config.PluginManagementCfg, opts loade
}
return ProvideService(cfg, pipeline.ProvideDiscoveryStage(cfg, reg),
pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), assets),
pipeline.ProvideBootstrapStage(cfg, signature.DefaultCalculator(cfg), pluginassets.NewLocalProvider()),
pipeline.ProvideValidationStage(cfg, signature.NewValidator(signature.NewUnsignedAuthorizer(cfg)), angularInspector),
pipeline.ProvideInitializationStage(cfg, reg, backendFactoryProvider, proc, authServiceRegistry, fakes.NewFakeRoleRegistry(), fakes.NewFakeActionSetRegistry(), fakes.NewFakePluginEnvProvider(), tracing.InitializeTracerForTest(), provisionedplugins.NewNoop()),
terminate, errTracker)

View File

@ -10,7 +10,6 @@ import (
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/envvars"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"
@ -19,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/process"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/manager/signature"
"github.com/grafana/grafana/pkg/plugins/pluginassets"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins"
)
@ -42,7 +42,7 @@ func ProvideDiscoveryStage(cfg *config.PluginManagementCfg, pr registry.Service)
})
}
func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.SignatureCalculator, a *assetpath.Service) *bootstrap.Bootstrap {
func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.SignatureCalculator, ap pluginassets.Provider) *bootstrap.Bootstrap {
disableAlertingForTempoDecorateFunc := func(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
if p.ID == coreplugin.Tempo && !cfg.Features.TempoAlertingEnabled {
p.Alerting = false
@ -51,7 +51,7 @@ func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.Signature
}
return bootstrap.New(cfg, bootstrap.Opts{
ConstructFunc: bootstrap.DefaultConstructFunc(cfg, sc, a),
ConstructFunc: bootstrap.DefaultConstructFunc(cfg, sc, ap),
DecorateFuncs: append(bootstrap.DefaultDecorateFuncs(cfg), disableAlertingForTempoDecorateFunc),
})
}

View File

@ -32,7 +32,6 @@ func ProvidePluginManagementConfig(cfg *setting.Cfg, settingProvider setting.Pro
SkipHostEnvVarsEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSkipHostEnvVars),
SriChecksEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSriChecks),
TempoAlertingEnabled: features.IsEnabledGlobally(featuremgmt.FlagTempoAlerting),
PluginAssetProvider: features.IsEnabledGlobally(featuremgmt.FlagPluginAssetProvider),
},
cfg.GrafanaComAPIURL,
cfg.DisablePlugins,

View File

@ -18,7 +18,6 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/filestore"
pluginLoader "github.com/grafana/grafana/pkg/plugins/manager/loader"
pAngularInspector "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"
@ -78,7 +77,6 @@ var WireSet = wire.NewSet(
wire.Bind(new(process.Manager), new(*process.Service)),
coreplugin.ProvideCoreRegistry,
pluginscdn.ProvideService,
assetpath.ProvideService,
pipeline.ProvideDiscoveryStage,
wire.Bind(new(discovery.Discoverer), new(*discovery.Discovery)),
@ -155,7 +153,7 @@ var WireExtensionSet = wire.NewSet(
wire.Bind(new(sources.Registry), new(*sources.Service)),
checkregistry.ProvideService,
wire.Bind(new(checkregistry.CheckService), new(*checkregistry.Service)),
pluginassets2.ProvideService,
pluginassets2.NewLocalProvider,
wire.Bind(new(pluginassets2.Provider), new(*pluginassets2.LocalProvider)),
)

View File

@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/manager/loader"
"github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery"
"github.com/grafana/grafana/pkg/plugins/manager/pipeline/initialization"
@ -27,7 +26,6 @@ import (
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
"github.com/grafana/grafana/pkg/plugins/manager/sources"
"github.com/grafana/grafana/pkg/plugins/pluginassets"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
@ -47,13 +45,12 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core
pCfg, err := pluginconfig.ProvidePluginManagementConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
cdn := pluginscdn.ProvideService(pCfg)
reg := registry.ProvideService()
angularInspector := angularinspector.NewStaticInspector()
proc := process.ProvideService()
disc := pipeline.ProvideDiscoveryStage(pCfg, reg)
boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), assetpath.ProvideService(pCfg, cdn, pluginassets.ProvideService()))
boot := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(pCfg, statickey.New()), pluginassets.NewLocalProvider())
valid := pipeline.ProvideValidationStage(pCfg, signature.NewValidator(signature.NewUnsignedAuthorizer(pCfg)), angularInspector)
init := pipeline.ProvideInitializationStage(pCfg, reg, provider.ProvideService(coreRegistry), proc, &fakes.FakeAuthService{}, fakes.NewFakeRoleRegistry(), fakes.NewFakeActionSetRegistry(), nil, tracing.InitializeTracerForTest(), provisionedplugins.NewNoop())
term, err := pipeline.ProvideTerminationStage(pCfg, reg, proc)
@ -91,7 +88,7 @@ func CreateTestLoader(t *testing.T, cfg *pluginsCfg.PluginManagementCfg, opts Lo
}
if opts.Bootstrapper == nil {
opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), assetpath.ProvideService(cfg, pluginscdn.ProvideService(cfg), pluginassets.ProvideService()))
opts.Bootstrapper = pipeline.ProvideBootstrapStage(cfg, signature.ProvideService(cfg, statickey.New()), pluginassets.NewLocalProvider())
}
if opts.Validator == nil {