Skip to content

Commit aea8b60

Browse files
authored
Plugins: Add support for fetching plugin includes from plugin CDN (grafana#91476)
* update oss side * add Rel func to plugins.FS * update tests * add comment * fix fs paths for nested plugin * fix test * fix sources * fix cdn class bug * update tests * remove commented out code
1 parent e7c628f commit aea8b60

File tree

12 files changed

+183
-47
lines changed

12 files changed

+183
-47
lines changed

pkg/plugins/ifaces.go

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ type FS interface {
6969

7070
Base() string
7171
Files() ([]string, error)
72+
Rel(string) (string, error)
7273
}
7374

7475
type FSRemover interface {

pkg/plugins/localfiles.go

+4
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ func (f LocalFS) fileIsAllowed(basePath string, absolutePath string, info os.Fil
7777
return true, nil
7878
}
7979

80+
func (f LocalFS) Rel(p string) (string, error) {
81+
return filepath.Rel(f.basePath, p)
82+
}
83+
8084
// walkFunc returns a filepath.WalkFunc that accumulates absolute file paths into acc by walking over f.Base().
8185
// f.fileIsAllowed is used as WalkFunc, see its documentation for more information on which files are collected.
8286
func (f LocalFS) walkFunc(basePath string, acc map[string]struct{}) filepath.WalkFunc {

pkg/plugins/manager/fakes/fakes.go

+15-7
Original file line numberDiff line numberDiff line change
@@ -403,35 +403,43 @@ func (f *FakeActionSetRegistry) RegisterActionSets(_ context.Context, _ string,
403403
return f.ExpectedErr
404404
}
405405

406-
type FakePluginFiles struct {
406+
type FakePluginFS struct {
407407
OpenFunc func(name string) (fs.File, error)
408408
RemoveFunc func() error
409+
RelFunc func(string) (string, error)
409410

410411
base string
411412
}
412413

413-
func NewFakePluginFiles(base string) *FakePluginFiles {
414-
return &FakePluginFiles{
414+
func NewFakePluginFS(base string) *FakePluginFS {
415+
return &FakePluginFS{
415416
base: base,
416417
}
417418
}
418419

419-
func (f *FakePluginFiles) Open(name string) (fs.File, error) {
420+
func (f *FakePluginFS) Open(name string) (fs.File, error) {
420421
if f.OpenFunc != nil {
421422
return f.OpenFunc(name)
422423
}
423424
return nil, nil
424425
}
425426

426-
func (f *FakePluginFiles) Base() string {
427+
func (f *FakePluginFS) Rel(_ string) (string, error) {
428+
if f.RelFunc != nil {
429+
return f.RelFunc(f.base)
430+
}
431+
return "", nil
432+
}
433+
434+
func (f *FakePluginFS) Base() string {
427435
return f.base
428436
}
429437

430-
func (f *FakePluginFiles) Files() ([]string, error) {
438+
func (f *FakePluginFS) Files() ([]string, error) {
431439
return []string{}, nil
432440
}
433441

434-
func (f *FakePluginFiles) Remove() error {
442+
func (f *FakePluginFS) Remove() error {
435443
if f.RemoveFunc != nil {
436444
return f.RemoveFunc()
437445
}

pkg/plugins/manager/loader/assetpath/assetpath.go

+52-6
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@ func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) *S
2727
type PluginInfo struct {
2828
pluginJSON plugins.JSONData
2929
class plugins.Class
30-
dir string
30+
fs plugins.FS
31+
parent *PluginInfo
3132
}
3233

33-
func NewPluginInfo(pluginJSON plugins.JSONData, class plugins.Class, fs plugins.FS) PluginInfo {
34+
func NewPluginInfo(pluginJSON plugins.JSONData, class plugins.Class, fs plugins.FS, parent *PluginInfo) PluginInfo {
3435
return PluginInfo{
3536
pluginJSON: pluginJSON,
3637
class: class,
37-
dir: fs.Base(),
38+
fs: fs,
39+
parent: parent,
3840
}
3941
}
4042

@@ -45,33 +47,77 @@ func DefaultService(cfg *config.PluginManagementCfg) *Service {
4547
// Base returns the base path for the specified plugin.
4648
func (s *Service) Base(n PluginInfo) (string, error) {
4749
if n.class == plugins.ClassCore {
48-
baseDir := getBaseDir(n.dir)
50+
baseDir := getBaseDir(n.fs.Base())
4951
return path.Join("public/app/plugins", string(n.pluginJSON.Type), baseDir), nil
5052
}
53+
if n.class == plugins.ClassCDN {
54+
return n.fs.Base(), nil
55+
}
5156
if s.cdn.PluginSupported(n.pluginJSON.ID) {
5257
return s.cdn.AssetURL(n.pluginJSON.ID, n.pluginJSON.Info.Version, "")
5358
}
59+
if n.parent != nil {
60+
if s.cdn.PluginSupported(n.parent.pluginJSON.ID) {
61+
relPath, err := n.parent.fs.Rel(n.fs.Base())
62+
if err != nil {
63+
return "", err
64+
}
65+
return s.cdn.AssetURL(n.parent.pluginJSON.ID, n.parent.pluginJSON.Info.Version, relPath)
66+
}
67+
}
68+
5469
return path.Join("public/plugins", n.pluginJSON.ID), nil
5570
}
5671

5772
// Module returns the module.js path for the specified plugin.
5873
func (s *Service) Module(n PluginInfo) (string, error) {
5974
if n.class == plugins.ClassCore {
60-
if filepath.Base(n.dir) == "dist" {
75+
if filepath.Base(n.fs.Base()) == "dist" {
6176
// The core plugin has been built externally, use the module from the dist folder
6277
} else {
63-
baseDir := getBaseDir(n.dir)
78+
baseDir := getBaseDir(n.fs.Base())
6479
return path.Join("core:plugin", baseDir), nil
6580
}
6681
}
82+
if n.class == plugins.ClassCDN {
83+
return pluginscdn.JoinPath(n.fs.Base(), "module.js")
84+
}
85+
6786
if s.cdn.PluginSupported(n.pluginJSON.ID) {
6887
return s.cdn.AssetURL(n.pluginJSON.ID, n.pluginJSON.Info.Version, "module.js")
6988
}
89+
if n.parent != nil {
90+
if s.cdn.PluginSupported(n.parent.pluginJSON.ID) {
91+
relPath, err := n.parent.fs.Rel(n.fs.Base())
92+
if err != nil {
93+
return "", err
94+
}
95+
return s.cdn.AssetURL(n.parent.pluginJSON.ID, n.parent.pluginJSON.Info.Version, path.Join(relPath, "module.js"))
96+
}
97+
}
98+
7099
return path.Join("public/plugins", n.pluginJSON.ID, "module.js"), nil
71100
}
72101

73102
// RelativeURL returns the relative URL for an arbitrary plugin asset.
74103
func (s *Service) RelativeURL(n PluginInfo, pathStr string) (string, error) {
104+
if n.class == plugins.ClassCDN {
105+
return pluginscdn.JoinPath(n.fs.Base(), pathStr)
106+
}
107+
108+
if s.cdn.PluginSupported(n.pluginJSON.ID) {
109+
return s.cdn.NewCDNURLConstructor(n.pluginJSON.ID, n.pluginJSON.Info.Version).StringPath(pathStr)
110+
}
111+
if n.parent != nil {
112+
if s.cdn.PluginSupported(n.parent.pluginJSON.ID) {
113+
relPath, err := n.parent.fs.Rel(n.fs.Base())
114+
if err != nil {
115+
return "", err
116+
}
117+
return s.cdn.AssetURL(n.parent.pluginJSON.ID, n.parent.pluginJSON.Info.Version, path.Join(relPath, pathStr))
118+
}
119+
}
120+
75121
if s.cdn.PluginSupported(n.pluginJSON.ID) {
76122
return s.cdn.NewCDNURLConstructor(n.pluginJSON.ID, n.pluginJSON.Info.Version).StringPath(pathStr)
77123
}

pkg/plugins/manager/loader/assetpath/assetpath_test.go

+80-18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package assetpath
22

33
import (
44
"net/url"
5+
"path"
56
"strings"
67
"testing"
78

@@ -13,8 +14,8 @@ import (
1314
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
1415
)
1516

16-
func extPath(pluginID string) *fakes.FakePluginFiles {
17-
return fakes.NewFakePluginFiles(pluginID)
17+
func pluginFS(basePath string) *fakes.FakePluginFS {
18+
return fakes.NewFakePluginFS(basePath)
1819
}
1920

2021
func TestService(t *testing.T) {
@@ -45,7 +46,7 @@ func TestService(t *testing.T) {
4546
}
4647
svc := ProvideService(cfg, pluginscdn.ProvideService(cfg))
4748

48-
tableOldFS := fakes.NewFakePluginFiles("/grafana/public/app/plugins/panel/table-old")
49+
tableOldFS := fakes.NewFakePluginFS("/grafana/public/app/plugins/panel/table-old")
4950
jsonData := map[string]plugins.JSONData{
5051
"table-old": {ID: "table-old", Info: plugins.Info{Version: "1.0.0"}},
5152

@@ -60,37 +61,75 @@ func TestService(t *testing.T) {
6061
})
6162

6263
t.Run("Base", func(t *testing.T) {
63-
base, err := svc.Base(NewPluginInfo(jsonData["one"], plugins.ClassExternal, extPath("one")))
64+
base, err := svc.Base(NewPluginInfo(jsonData["one"], plugins.ClassExternal, pluginFS("one"), nil))
6465
require.NoError(t, err)
6566

66-
u, err := url.JoinPath(tc.cdnBaseURL, "/one/1.0.0/public/plugins/one")
67+
oneCDNURL, err := url.JoinPath(tc.cdnBaseURL, "/one/1.0.0/public/plugins/one")
6768
require.NoError(t, err)
68-
require.Equal(t, u, base)
69+
require.Equal(t, oneCDNURL, base)
6970

70-
base, err = svc.Base(NewPluginInfo(jsonData["two"], plugins.ClassExternal, extPath("two")))
71+
base, err = svc.Base(NewPluginInfo(jsonData["one"], plugins.ClassCDN, pluginFS(oneCDNURL), nil))
72+
require.NoError(t, err)
73+
require.Equal(t, oneCDNURL, base)
74+
75+
base, err = svc.Base(NewPluginInfo(jsonData["two"], plugins.ClassExternal, pluginFS("two"), nil))
7176
require.NoError(t, err)
7277
require.Equal(t, "public/plugins/two", base)
7378

74-
base, err = svc.Base(NewPluginInfo(jsonData["table-old"], plugins.ClassCore, tableOldFS))
79+
base, err = svc.Base(NewPluginInfo(jsonData["table-old"], plugins.ClassCore, tableOldFS, nil))
7580
require.NoError(t, err)
7681
require.Equal(t, "public/app/plugins/table-old", base)
82+
83+
parentFS := pluginFS(oneCDNURL)
84+
parentFS.RelFunc = func(_ string) (string, error) {
85+
return "child-plugins/two", nil
86+
}
87+
parent := NewPluginInfo(jsonData["one"], plugins.ClassExternal, parentFS, nil)
88+
child := NewPluginInfo(jsonData["two"], plugins.ClassExternal, fakes.NewFakePluginFS(""), &parent)
89+
base, err = svc.Base(child)
90+
require.NoError(t, err)
91+
92+
childBase, err := url.JoinPath(oneCDNURL, "child-plugins/two")
93+
require.NoError(t, err)
94+
require.Equal(t, childBase, base)
7795
})
7896

7997
t.Run("Module", func(t *testing.T) {
80-
module, err := svc.Module(NewPluginInfo(jsonData["one"], plugins.ClassExternal, extPath("one")))
98+
module, err := svc.Module(NewPluginInfo(jsonData["one"], plugins.ClassExternal, pluginFS("one"), nil))
8199
require.NoError(t, err)
82100

83-
u, err := url.JoinPath(tc.cdnBaseURL, "/one/1.0.0/public/plugins/one/module.js")
101+
oneCDNURL, err := url.JoinPath(tc.cdnBaseURL, "/one/1.0.0/public/plugins/one")
84102
require.NoError(t, err)
85-
require.Equal(t, u, module)
86103

87-
module, err = svc.Module(NewPluginInfo(jsonData["two"], plugins.ClassExternal, extPath("two")))
104+
oneCDNModuleURL, err := url.JoinPath(oneCDNURL, "module.js")
105+
require.NoError(t, err)
106+
require.Equal(t, oneCDNModuleURL, module)
107+
108+
fs := pluginFS("one")
109+
module, err = svc.Module(NewPluginInfo(jsonData["one"], plugins.ClassCDN, fs, nil))
110+
require.NoError(t, err)
111+
require.Equal(t, path.Join(fs.Base(), "module.js"), module)
112+
113+
module, err = svc.Module(NewPluginInfo(jsonData["two"], plugins.ClassExternal, pluginFS("two"), nil))
88114
require.NoError(t, err)
89115
require.Equal(t, "public/plugins/two/module.js", module)
90116

91-
module, err = svc.Module(NewPluginInfo(jsonData["table-old"], plugins.ClassCore, tableOldFS))
117+
module, err = svc.Module(NewPluginInfo(jsonData["table-old"], plugins.ClassCore, tableOldFS, nil))
92118
require.NoError(t, err)
93119
require.Equal(t, "core:plugin/table-old", module)
120+
121+
parentFS := pluginFS(oneCDNURL)
122+
parentFS.RelFunc = func(_ string) (string, error) {
123+
return "child-plugins/two", nil
124+
}
125+
parent := NewPluginInfo(jsonData["one"], plugins.ClassExternal, parentFS, nil)
126+
child := NewPluginInfo(jsonData["two"], plugins.ClassExternal, fakes.NewFakePluginFS(""), &parent)
127+
module, err = svc.Module(child)
128+
require.NoError(t, err)
129+
130+
childModule, err := url.JoinPath(oneCDNURL, "child-plugins/two/module.js")
131+
require.NoError(t, err)
132+
require.Equal(t, childModule, module)
94133
})
95134

96135
t.Run("RelativeURL", func(t *testing.T) {
@@ -103,24 +142,47 @@ func TestService(t *testing.T) {
103142
},
104143
}
105144

106-
u, err := svc.RelativeURL(NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassExternal, extPath("one")), "")
145+
u, err := svc.RelativeURL(NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassExternal, pluginFS("one"), nil), "")
107146
require.NoError(t, err)
108147
// given an empty path, base URL will be returned
109-
baseURL, err := svc.Base(NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassExternal, extPath("one")))
148+
baseURL, err := svc.Base(NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassExternal, pluginFS("one"), nil))
110149
require.NoError(t, err)
111150
require.Equal(t, baseURL, u)
112151

113-
u, err = svc.RelativeURL(NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassExternal, extPath("one")), "path/to/file.txt")
152+
u, err = svc.RelativeURL(NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassExternal, pluginFS("one"), nil), "path/to/file.txt")
114153
require.NoError(t, err)
115154
require.Equal(t, strings.TrimRight(tc.cdnBaseURL, "/")+"/one/1.0.0/public/plugins/one/path/to/file.txt", u)
116155

117-
u, err = svc.RelativeURL(NewPluginInfo(pluginsMap["two"].JSONData, plugins.ClassExternal, extPath("two")), "path/to/file.txt")
156+
u, err = svc.RelativeURL(NewPluginInfo(pluginsMap["two"].JSONData, plugins.ClassExternal, pluginFS("two"), nil), "path/to/file.txt")
118157
require.NoError(t, err)
119158
require.Equal(t, "public/plugins/two/path/to/file.txt", u)
120159

121-
u, err = svc.RelativeURL(NewPluginInfo(pluginsMap["two"].JSONData, plugins.ClassExternal, extPath("two")), "default")
160+
u, err = svc.RelativeURL(NewPluginInfo(pluginsMap["two"].JSONData, plugins.ClassExternal, pluginFS("two"), nil), "default")
122161
require.NoError(t, err)
123162
require.Equal(t, "public/plugins/two/default", u)
163+
164+
oneCDNURL, err := url.JoinPath(tc.cdnBaseURL, "/one/1.0.0/public/plugins/one")
165+
require.NoError(t, err)
166+
167+
u, err = svc.RelativeURL(NewPluginInfo(pluginsMap["one"].JSONData, plugins.ClassCDN, pluginFS(oneCDNURL), nil), "path/to/file.txt")
168+
require.NoError(t, err)
169+
170+
oneCDNRelativeURL, err := url.JoinPath(oneCDNURL, "path/to/file.txt")
171+
require.NoError(t, err)
172+
require.Equal(t, oneCDNRelativeURL, u)
173+
174+
parentFS := pluginFS(oneCDNURL)
175+
parentFS.RelFunc = func(_ string) (string, error) {
176+
return "child-plugins/two", nil
177+
}
178+
parent := NewPluginInfo(jsonData["one"], plugins.ClassExternal, parentFS, nil)
179+
child := NewPluginInfo(jsonData["two"], plugins.ClassExternal, fakes.NewFakePluginFS(""), &parent)
180+
u, err = svc.RelativeURL(child, "path/to/file.txt")
181+
require.NoError(t, err)
182+
183+
oneCDNRelativeURL, err = url.JoinPath(oneCDNURL, "child-plugins/two/path/to/file.txt")
184+
require.NoError(t, err)
185+
require.Equal(t, oneCDNRelativeURL, u)
124186
})
125187
})
126188
}

pkg/plugins/manager/pipeline/bootstrap/factory.go

+8-7
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ func NewDefaultPluginFactory(assetPath *assetpath.Service) *DefaultPluginFactory
2525

2626
func (f *DefaultPluginFactory) createPlugin(bundle *plugins.FoundBundle, class plugins.Class,
2727
sig plugins.Signature) (*plugins.Plugin, error) {
28-
plugin, err := f.newPlugin(bundle.Primary, class, sig)
28+
parentInfo := assetpath.NewPluginInfo(bundle.Primary.JSONData, class, bundle.Primary.FS, nil)
29+
plugin, err := f.newPlugin(bundle.Primary, class, sig, parentInfo)
2930
if err != nil {
3031
return nil, err
3132
}
@@ -36,7 +37,8 @@ func (f *DefaultPluginFactory) createPlugin(bundle *plugins.FoundBundle, class p
3637

3738
plugin.Children = make([]*plugins.Plugin, 0, len(bundle.Children))
3839
for _, child := range bundle.Children {
39-
cp, err := f.newPlugin(*child, class, sig)
40+
childInfo := assetpath.NewPluginInfo(child.JSONData, class, child.FS, &parentInfo)
41+
cp, err := f.newPlugin(*child, class, sig, childInfo)
4042
if err != nil {
4143
return nil, err
4244
}
@@ -47,8 +49,8 @@ func (f *DefaultPluginFactory) createPlugin(bundle *plugins.FoundBundle, class p
4749
return plugin, nil
4850
}
4951

50-
func (f *DefaultPluginFactory) newPlugin(p plugins.FoundPlugin, class plugins.Class, sig plugins.Signature) (*plugins.Plugin, error) {
51-
info := assetpath.NewPluginInfo(p.JSONData, class, p.FS)
52+
func (f *DefaultPluginFactory) newPlugin(p plugins.FoundPlugin, class plugins.Class, sig plugins.Signature,
53+
info assetpath.PluginInfo) (*plugins.Plugin, error) {
5254
baseURL, err := f.assetPath.Base(info)
5355
if err != nil {
5456
return nil, fmt.Errorf("base url: %w", err)
@@ -69,14 +71,13 @@ func (f *DefaultPluginFactory) newPlugin(p plugins.FoundPlugin, class plugins.Cl
6971
}
7072

7173
plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID)))
72-
if err = setImages(plugin, f.assetPath); err != nil {
74+
if err = setImages(plugin, f.assetPath, info); err != nil {
7375
return nil, err
7476
}
7577
return plugin, nil
7678
}
7779

78-
func setImages(p *plugins.Plugin, assetPath *assetpath.Service) error {
79-
info := assetpath.NewPluginInfo(p.JSONData, p.Class, p.FS)
80+
func setImages(p *plugins.Plugin, assetPath *assetpath.Service, info assetpath.PluginInfo) error {
8081
var err error
8182
for _, dst := range []*string{&p.Info.Logos.Small, &p.Info.Logos.Large} {
8283
if len(*dst) == 0 {

0 commit comments

Comments
 (0)