mirror of
https://github.com/grafana/grafana.git
synced 2026-08-11 13:44:59 -05:00
Plugins: Add file store abstraction for handling plugin files (#65432)
* add file store * fix markdown fetch bug * add markdown tests * fix var name
This commit is contained in:
+76
-96
@@ -1,13 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -27,6 +25,9 @@ import (
|
||||
"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/manager/filestore"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/store"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
@@ -140,6 +141,7 @@ func Test_PluginsInstallAndUninstall_AccessControl(t *testing.T) {
|
||||
PluginAdminExternalManageEnabled: tc.pluginAdminExternalManageEnabled}
|
||||
hs.orgService = &orgtest.FakeOrgService{ExpectedOrg: &org.Org{}}
|
||||
hs.pluginInstaller = NewFakePluginInstaller()
|
||||
hs.pluginFileStore = &fakes.FakePluginFileStore{}
|
||||
})
|
||||
|
||||
t.Run(testName("Install", tc), func(t *testing.T) {
|
||||
@@ -172,10 +174,10 @@ func Test_GetPluginAssetCDNRedirect(t *testing.T) {
|
||||
nonCdnPlugin := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: nonCDNPluginID, Info: plugins.Info{Version: "2.0.0"}},
|
||||
}
|
||||
service := &plugins.FakePluginStore{
|
||||
PluginList: []plugins.PluginDTO{
|
||||
cdnPlugin.ToDTO(),
|
||||
nonCdnPlugin.ToDTO(),
|
||||
registry := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
cdnPluginID: cdnPlugin,
|
||||
nonCDNPluginID: nonCdnPlugin,
|
||||
},
|
||||
}
|
||||
cfg := setting.NewCfg()
|
||||
@@ -200,7 +202,7 @@ func Test_GetPluginAssetCDNRedirect(t *testing.T) {
|
||||
"When calling GET for a CDN plugin on",
|
||||
fmt.Sprintf("/public/plugins/%s/%s", cdnPluginID, cas.assetURL),
|
||||
"/public/plugins/:pluginId/*",
|
||||
cfg, service, func(sc *scenarioContext) {
|
||||
cfg, registry, func(sc *scenarioContext) {
|
||||
// Get the prometheus metric (to test that the handler is instrumented correctly)
|
||||
counter := pluginsCDNFallbackRedirectRequests.With(prometheus.Labels{
|
||||
"plugin_id": cdnPluginID,
|
||||
@@ -230,7 +232,7 @@ func Test_GetPluginAssetCDNRedirect(t *testing.T) {
|
||||
"When calling GET for a non-CDN plugin on",
|
||||
fmt.Sprintf("/public/plugins/%s/%s", nonCDNPluginID, "module.js"),
|
||||
"/public/plugins/:pluginId/*",
|
||||
cfg, service, func(sc *scenarioContext) {
|
||||
cfg, registry, func(sc *scenarioContext) {
|
||||
// Here the metric should not increment
|
||||
var m dto.Metric
|
||||
counter := pluginsCDNFallbackRedirectRequests.With(prometheus.Labels{
|
||||
@@ -275,14 +277,16 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
requestedFile := filepath.Clean(tmpFile.Name())
|
||||
|
||||
t.Run("Given a request for an existing plugin file", func(t *testing.T) {
|
||||
p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewLocalFS(map[string]struct{}{requestedFile: {}}, filepath.Dir(requestedFile)))
|
||||
service := &plugins.FakePluginStore{
|
||||
PluginList: []plugins.PluginDTO{p},
|
||||
p := createPlugin(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewLocalFS(map[string]struct{}{requestedFile: {}}, filepath.Dir(requestedFile)))
|
||||
pluginRegistry := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
p.ID: p,
|
||||
},
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile)
|
||||
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*",
|
||||
setting.NewCfg(), service, func(sc *scenarioContext) {
|
||||
setting.NewCfg(), pluginRegistry, func(sc *scenarioContext) {
|
||||
callGetPluginAsset(sc)
|
||||
|
||||
require.Equal(t, 200, sc.resp.Code)
|
||||
@@ -291,14 +295,16 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Given a request for a relative path", func(t *testing.T) {
|
||||
p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewLocalFS(map[string]struct{}{}, ""))
|
||||
service := &plugins.FakePluginStore{
|
||||
PluginList: []plugins.PluginDTO{p},
|
||||
p := createPlugin(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewLocalFS(map[string]struct{}{}, ""))
|
||||
pluginRegistry := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
p.ID: p,
|
||||
},
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, tmpFileInParentDir.Name())
|
||||
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*",
|
||||
setting.NewCfg(), service, func(sc *scenarioContext) {
|
||||
setting.NewCfg(), pluginRegistry, func(sc *scenarioContext) {
|
||||
callGetPluginAsset(sc)
|
||||
|
||||
require.Equal(t, 404, sc.resp.Code)
|
||||
@@ -306,16 +312,18 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Given a request for an existing plugin file that is not listed as a signature covered file", func(t *testing.T) {
|
||||
p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.Core, plugins.NewLocalFS(map[string]struct{}{
|
||||
p := createPlugin(plugins.JSONData{ID: pluginID}, plugins.Core, plugins.NewLocalFS(map[string]struct{}{
|
||||
requestedFile: {},
|
||||
}, ""))
|
||||
service := &plugins.FakePluginStore{
|
||||
PluginList: []plugins.PluginDTO{p},
|
||||
pluginRegistry := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
p.ID: p,
|
||||
},
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile)
|
||||
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*",
|
||||
setting.NewCfg(), service, func(sc *scenarioContext) {
|
||||
setting.NewCfg(), pluginRegistry, func(sc *scenarioContext) {
|
||||
callGetPluginAsset(sc)
|
||||
|
||||
require.Equal(t, 200, sc.resp.Code)
|
||||
@@ -324,9 +332,11 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Given a request for an non-existing plugin file", func(t *testing.T) {
|
||||
p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewLocalFS(map[string]struct{}{}, ""))
|
||||
service := &plugins.FakePluginStore{
|
||||
PluginList: []plugins.PluginDTO{p},
|
||||
p := createPlugin(plugins.JSONData{ID: pluginID}, plugins.External, plugins.NewLocalFS(map[string]struct{}{}, ""))
|
||||
service := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
p.ID: p,
|
||||
},
|
||||
}
|
||||
|
||||
requestedFile := "nonExistent"
|
||||
@@ -344,14 +354,10 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Given a request for an non-existing plugin", func(t *testing.T) {
|
||||
service := &plugins.FakePluginStore{
|
||||
PluginList: []plugins.PluginDTO{},
|
||||
}
|
||||
|
||||
requestedFile := "nonExistent"
|
||||
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile)
|
||||
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*",
|
||||
setting.NewCfg(), service, func(sc *scenarioContext) {
|
||||
setting.NewCfg(), fakes.NewFakePluginRegistry(), func(sc *scenarioContext) {
|
||||
callGetPluginAsset(sc)
|
||||
|
||||
var respJson map[string]interface{}
|
||||
@@ -471,11 +477,12 @@ func TestMakePluginResourceRequestContentTypeEmpty(t *testing.T) {
|
||||
|
||||
func TestPluginMarkdown(t *testing.T) {
|
||||
t.Run("Plugin not installed returns error", func(t *testing.T) {
|
||||
hs := HTTPServer{
|
||||
pluginStore: &plugins.FakePluginStore{
|
||||
PluginList: []plugins.PluginDTO{},
|
||||
pluginFileStore := &fakes.FakePluginFileStore{
|
||||
FileFunc: func(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
|
||||
return nil, plugins.ErrPluginNotInstalled
|
||||
},
|
||||
}
|
||||
hs := HTTPServer{pluginFileStore: pluginFileStore}
|
||||
|
||||
pluginID := "test-datasource"
|
||||
md, err := hs.pluginMarkdown(context.Background(), pluginID, "test")
|
||||
@@ -485,20 +492,16 @@ func TestPluginMarkdown(t *testing.T) {
|
||||
|
||||
t.Run("File fetch will be retried using different casing if error occurs", func(t *testing.T) {
|
||||
var requestedFiles []string
|
||||
pluginFiles := &fakes.FakePluginFiles{
|
||||
OpenFunc: func(name string) (fs.File, error) {
|
||||
requestedFiles = append(requestedFiles, name)
|
||||
pluginFileStore := &fakes.FakePluginFileStore{
|
||||
FileFunc: func(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
|
||||
requestedFiles = append(requestedFiles, filename)
|
||||
return nil, errors.New("some error")
|
||||
},
|
||||
}
|
||||
|
||||
p := createPluginDTO(plugins.JSONData{ID: "test-app"}, plugins.External, pluginFiles)
|
||||
hs := HTTPServer{pluginFileStore: pluginFileStore}
|
||||
|
||||
hs := HTTPServer{
|
||||
pluginStore: &plugins.FakePluginStore{PluginList: []plugins.PluginDTO{p}},
|
||||
}
|
||||
|
||||
md, err := hs.pluginMarkdown(context.Background(), p.ID, "reAdMe")
|
||||
md, err := hs.pluginMarkdown(context.Background(), "", "reAdMe")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte{}, md)
|
||||
require.Equal(t, []string{"README.md", "readme.md"}, requestedFiles)
|
||||
@@ -524,54 +527,44 @@ func TestPluginMarkdown(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
data := []byte{123}
|
||||
var requestedFiles []string
|
||||
pluginFiles := &fakes.FakePluginFiles{
|
||||
OpenFunc: func(name string) (fs.File, error) {
|
||||
requestedFiles = append(requestedFiles, name)
|
||||
return &FakeFile{data: bytes.NewReader(nil)}, nil
|
||||
pluginFileStore := &fakes.FakePluginFileStore{
|
||||
FileFunc: func(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
|
||||
requestedFiles = append(requestedFiles, filename)
|
||||
return &plugins.File{Content: data}, nil
|
||||
},
|
||||
}
|
||||
|
||||
p := createPluginDTO(plugins.JSONData{ID: "test-app"}, plugins.External, pluginFiles)
|
||||
hs := HTTPServer{pluginFileStore: pluginFileStore}
|
||||
|
||||
hs := HTTPServer{
|
||||
pluginStore: &plugins.FakePluginStore{PluginList: []plugins.PluginDTO{p}},
|
||||
}
|
||||
|
||||
md, err := hs.pluginMarkdown(context.Background(), p.ID, tc.filePath)
|
||||
md, err := hs.pluginMarkdown(context.Background(), "test-datasource", tc.filePath)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte{}, md)
|
||||
require.Equal(t, data, md)
|
||||
require.Equal(t, tc.expected, requestedFiles)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Non markdown file request returns an error", func(t *testing.T) {
|
||||
p := createPluginDTO(plugins.JSONData{ID: "test-app"}, plugins.External, nil)
|
||||
hs := HTTPServer{
|
||||
pluginStore: &plugins.FakePluginStore{PluginList: []plugins.PluginDTO{p}},
|
||||
}
|
||||
hs := HTTPServer{pluginFileStore: &fakes.FakePluginFileStore{}}
|
||||
|
||||
md, err := hs.pluginMarkdown(context.Background(), p.ID, "test.json")
|
||||
md, err := hs.pluginMarkdown(context.Background(), "", "test.json")
|
||||
require.ErrorIs(t, err, ErrUnexpectedFileExtension)
|
||||
require.Equal(t, []byte{}, md)
|
||||
})
|
||||
|
||||
t.Run("Happy path", func(t *testing.T) {
|
||||
data := []byte{1, 2, 3}
|
||||
fakeFile := &FakeFile{data: bytes.NewReader(data)}
|
||||
|
||||
pluginFiles := &fakes.FakePluginFiles{
|
||||
OpenFunc: func(name string) (fs.File, error) {
|
||||
return fakeFile, nil
|
||||
pluginFileStore := &fakes.FakePluginFileStore{
|
||||
FileFunc: func(ctx context.Context, pluginID, filename string) (*plugins.File, error) {
|
||||
return &plugins.File{Content: data}, nil
|
||||
},
|
||||
}
|
||||
|
||||
p := createPluginDTO(plugins.JSONData{ID: "test-app"}, plugins.External, pluginFiles)
|
||||
hs := HTTPServer{
|
||||
pluginStore: &plugins.FakePluginStore{PluginList: []plugins.PluginDTO{p}},
|
||||
}
|
||||
hs := HTTPServer{pluginFileStore: pluginFileStore}
|
||||
|
||||
md, err := hs.pluginMarkdown(context.Background(), p.ID, "someFile")
|
||||
md, err := hs.pluginMarkdown(context.Background(), "", "someFile")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, data, md)
|
||||
})
|
||||
@@ -582,13 +575,14 @@ func callGetPluginAsset(sc *scenarioContext) {
|
||||
}
|
||||
|
||||
func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string,
|
||||
cfg *setting.Cfg, pluginStore plugins.Store, fn scenarioFunc) {
|
||||
cfg *setting.Cfg, pluginRegistry registry.Service, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
cfg.IsFeatureToggleEnabled = func(_ string) bool { return false }
|
||||
hs := HTTPServer{
|
||||
Cfg: cfg,
|
||||
pluginStore: pluginStore,
|
||||
log: log.NewNopLogger(),
|
||||
Cfg: cfg,
|
||||
pluginStore: store.New(pluginRegistry),
|
||||
pluginFileStore: filestore.ProvideService(pluginRegistry),
|
||||
log: log.NewNopLogger(),
|
||||
pluginsCDNService: pluginscdn.ProvideService(&config.Cfg{
|
||||
PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate,
|
||||
PluginSettings: cfg.PluginSettings,
|
||||
@@ -648,19 +642,24 @@ func (c *fakePluginClient) QueryData(ctx context.Context, req *backend.QueryData
|
||||
}
|
||||
|
||||
func Test_PluginsList_AccessControl(t *testing.T) {
|
||||
p1 := createPluginDTO(plugins.JSONData{
|
||||
p1 := createPlugin(plugins.JSONData{
|
||||
ID: "test-app", Type: "app", Name: "test-app",
|
||||
Info: plugins.Info{
|
||||
Version: "1.0.0",
|
||||
}}, plugins.External, plugins.NewLocalFS(map[string]struct{}{}, ""))
|
||||
p2 := createPluginDTO(
|
||||
p2 := createPlugin(
|
||||
plugins.JSONData{ID: "mysql", Type: "datasource", Name: "MySQL",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{Name: "Grafana Labs", URL: "https://grafana.com"},
|
||||
Description: "Data source for MySQL databases",
|
||||
}}, plugins.Core, plugins.NewLocalFS(map[string]struct{}{}, ""))
|
||||
|
||||
pluginStore := plugins.FakePluginStore{PluginList: []plugins.PluginDTO{p1, p2}}
|
||||
pluginRegistry := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
p1.ID: p1,
|
||||
p2.ID: p2,
|
||||
},
|
||||
}
|
||||
|
||||
pluginSettings := pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{
|
||||
"test-app": {ID: 0, OrgID: 1, PluginID: "test-app", PluginVersion: "1.0.0", Enabled: true},
|
||||
@@ -693,9 +692,10 @@ func Test_PluginsList_AccessControl(t *testing.T) {
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.Cfg = setting.NewCfg()
|
||||
hs.PluginSettings = &pluginSettings
|
||||
hs.pluginStore = pluginStore
|
||||
hs.pluginStore = store.New(pluginRegistry)
|
||||
hs.pluginFileStore = filestore.ProvideService(pluginRegistry)
|
||||
var err error
|
||||
hs.pluginsUpdateChecker, err = updatechecker.ProvidePluginsService(hs.Cfg, pluginStore, tracing.InitializeTracerForTest())
|
||||
hs.pluginsUpdateChecker, err = updatechecker.ProvidePluginsService(hs.Cfg, nil, tracing.InitializeTracerForTest())
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@@ -713,30 +713,10 @@ func Test_PluginsList_AccessControl(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func createPluginDTO(jd plugins.JSONData, class plugins.Class, files plugins.FS) plugins.PluginDTO {
|
||||
p := &plugins.Plugin{
|
||||
func createPlugin(jd plugins.JSONData, class plugins.Class, files plugins.FS) *plugins.Plugin {
|
||||
return &plugins.Plugin{
|
||||
JSONData: jd,
|
||||
Class: class,
|
||||
FS: files,
|
||||
}
|
||||
|
||||
return p.ToDTO()
|
||||
}
|
||||
|
||||
var _ fs.File = (*FakeFile)(nil)
|
||||
|
||||
type FakeFile struct {
|
||||
data io.Reader
|
||||
}
|
||||
|
||||
func (f *FakeFile) Stat() (fs.FileInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *FakeFile) Read(bytes []byte) (int, error) {
|
||||
return f.data.Read(bytes)
|
||||
}
|
||||
|
||||
func (f *FakeFile) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user