mirror of
https://github.com/grafana/grafana.git
synced 2026-08-11 05:34:53 -05:00
Plugins: Refactor Plugin Management (#40477)
* add core plugin flow * add instrumentation * move func * remove cruft * support external backend plugins * refactor + clean up * remove comments * refactor loader * simplify core plugin path arg * cleanup loggers * move signature validator to plugins package * fix sig packaging * cleanup plugin model * remove unnecessary plugin field * add start+stop for pm * fix failures * add decommissioned state * export fields just to get things flowing * fix comments * set static routes * make image loading idempotent * merge with backend plugin manager * re-use funcs * reorder imports + remove unnecessary interface * add some TODOs + remove unused func * remove unused instrumentation func * simplify client usage * remove import alias * re-use backendplugin.Plugin interface * re order funcs * improve var name * fix log statements * refactor data model * add logic for dupe check during loading * cleanup state setting * refactor loader * cleanup manager interface * add rendering flow * refactor loading + init * add renderer support * fix renderer plugin * reformat imports * track errors * fix plugin signature inheritance * name param in interface * update func comment * fix func arg name * introduce class concept * remove func * fix external plugin check * apply changes from pm-experiment * fix core plugins * fix imports * rename interface * comment API interface * add support for testdata plugin * enable alerting + use correct core plugin contracts * slim manager API * fix param name * fix filter * support static routes * fix rendering * tidy rendering * get tests compiling * fix install+uninstall * start finder test * add finder test coverage * start loader tests * add test for core plugins * load core + bundled test * add test for nested plugin loading * add test files * clean interface + fix registering some core plugins * refactoring * reformat and create sub packages * simplify core plugin init * fix ctx cancel scenario * migrate initializer * remove Init() funcs * add test starter * new logger * flesh out initializer tests * refactoring * remove unused svc * refactor rendering flow * fixup loader tests * add enabled helper func * fix logger name * fix data fetchers * fix case where plugin dir doesn't exist * improve coverage + move dupe checking to loader * remove noisy debug logs * register core plugins automagically * add support for renderer in catalog * make private func + fix req validation * use interface * re-add check for renderer in catalog * tidy up from moving to auto reg core plugins * core plugin registrar * guards * copy over core plugins for test infra * all tests green * renames * propagate new interfaces * kill old manager * get compiling * tidy up * update naming * refactor manager test + cleanup * add more cases to finder test * migrate validator to field * more coverage * refactor dupe checking * add test for plugin class * add coverage for initializer * split out rendering * move * fixup tests * fix uss test * fix frontend settings * fix grafanads test * add check when checking sig errors * fix enabled map * fixup * allow manual setup of CM * rename to cloud-monitoring * remove TODO * add installer interface for testing * loader interface returns * tests passing * refactor + add more coverage * support 'stackdriver' * fix frontend settings loading * improve naming based on package name * small tidy * refactor test * fix renderer start * make cloud-monitoring plugin ID clearer * add plugin update test * add integration tests * don't break all if sig can't be calculated * add root URL check test * add more signature verification tests * update DTO name * update enabled plugins comment * update comments * fix linter * revert fe naming change * fix errors endpoint * reset error code field name * re-order test to help verify * assert -> require * pm check * add missing entry + re-order * re-check * dump icon log * verify manager contents first * reformat * apply PR feedback * apply style changes * fix one vs all loading err * improve log output * only start when no signature error * move log * rework plugin update check * fix test * fix multi loading from cfg.PluginSettings * improve log output #2 * add error abstraction to capture errors without registering a plugin * add debug log * add unsigned warning * e2e test attempt * fix logger * set home path * prevent panic * alternate * ugh.. fix home path * return renderer even if not started * make renderer plugin managed * add fallback renderer icon, update renderer badge + prevent changes when renderer is installed * fix icon loading * rollback renderer changes * use correct field * remove unneccessary block * remove newline * remove unused func * fix bundled plugins base + module fields * remove unused field since refactor * add authorizer abstraction * loader only returns plugins expected to run * fix multi log output
This commit is contained in:
@@ -6,9 +6,7 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
)
|
||||
|
||||
var varRegex = regexp.MustCompile(`(\$\{.+?\})`)
|
||||
@@ -21,65 +19,6 @@ func (e DashboardInputMissingError) Error() string {
|
||||
return fmt.Sprintf("Dashboard input variable: %v missing from import command", e.VariableName)
|
||||
}
|
||||
|
||||
func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json,
|
||||
overwrite bool, inputs []plugins.ImportDashboardInput, user *models.SignedInUser,
|
||||
requestHandler plugins.DataRequestHandler) (plugins.PluginDashboardInfoDTO, *models.Dashboard, error) {
|
||||
var dashboard *models.Dashboard
|
||||
if pluginID != "" {
|
||||
var err error
|
||||
if dashboard, err = pm.LoadPluginDashboard(pluginID, path); err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
} else {
|
||||
dashboard = models.NewDashboardFromJson(dashboardModel)
|
||||
}
|
||||
|
||||
evaluator := &DashTemplateEvaluator{
|
||||
template: dashboard.Data,
|
||||
inputs: inputs,
|
||||
}
|
||||
|
||||
generatedDash, err := evaluator.Eval()
|
||||
if err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
|
||||
saveCmd := models.SaveDashboardCommand{
|
||||
Dashboard: generatedDash,
|
||||
OrgId: orgID,
|
||||
UserId: user.UserId,
|
||||
Overwrite: overwrite,
|
||||
PluginId: pluginID,
|
||||
FolderId: folderID,
|
||||
}
|
||||
|
||||
dto := &dashboards.SaveDashboardDTO{
|
||||
OrgId: orgID,
|
||||
Dashboard: saveCmd.GetDashboardModel(),
|
||||
Overwrite: saveCmd.Overwrite,
|
||||
User: user,
|
||||
}
|
||||
|
||||
savedDash, err := dashboards.NewService(pm.SQLStore).ImportDashboard(dto)
|
||||
if err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
|
||||
return plugins.PluginDashboardInfoDTO{
|
||||
PluginId: pluginID,
|
||||
Title: savedDash.Title,
|
||||
Path: path,
|
||||
Revision: savedDash.Data.Get("revision").MustInt64(1),
|
||||
FolderId: savedDash.FolderId,
|
||||
ImportedUri: "db/" + savedDash.Slug,
|
||||
ImportedUrl: savedDash.GetUrl(),
|
||||
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
|
||||
Imported: true,
|
||||
DashboardId: savedDash.Id,
|
||||
Slug: savedDash.Slug,
|
||||
}, savedDash, nil
|
||||
}
|
||||
|
||||
type DashTemplateEvaluator struct {
|
||||
template *simplejson.Json
|
||||
inputs []plugins.ImportDashboardInput
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -25,7 +27,7 @@ func TestDashboardImport(t *testing.T) {
|
||||
info, dash, err := pm.ImportDashboard("test-app", "dashboards/connections.json", 1, 0, nil, false,
|
||||
[]plugins.ImportDashboardInput{
|
||||
{Name: "*", Type: "datasource", Value: "graphite"},
|
||||
}, &models.SignedInUser{UserId: 1, OrgRole: models.ROLE_ADMIN}, nil)
|
||||
}, &models.SignedInUser{UserId: 1, OrgRole: models.ROLE_ADMIN})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
require.NotNil(t, dash)
|
||||
@@ -88,7 +90,7 @@ func pluginScenario(t *testing.T, desc string, fn func(*testing.T, *PluginManage
|
||||
},
|
||||
},
|
||||
}
|
||||
pm := newManager(cfg, &sqlstore.SQLStore{}, &fakeBackendPluginManager{})
|
||||
pm := newManager(cfg, nil, loader.New(nil, cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}), &sqlstore.SQLStore{})
|
||||
err := pm.init()
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
)
|
||||
|
||||
func (pm *PluginManager) GetPluginDashboards(orgID int64, pluginID string) ([]*plugins.PluginDashboardInfoDTO, error) {
|
||||
plugin := pm.GetPlugin(pluginID)
|
||||
func (m *PluginManager) GetPluginDashboards(orgID int64, pluginID string) ([]*plugins.PluginDashboardInfoDTO, error) {
|
||||
plugin := m.Plugin(pluginID)
|
||||
if plugin == nil {
|
||||
return nil, plugins.PluginNotFoundError{PluginID: pluginID}
|
||||
return nil, plugins.NotFoundError{PluginID: pluginID}
|
||||
}
|
||||
|
||||
result := make([]*plugins.PluginDashboardInfoDTO, 0)
|
||||
@@ -26,18 +27,18 @@ func (pm *PluginManager) GetPluginDashboards(orgID int64, pluginID string) ([]*p
|
||||
|
||||
existingMatches := make(map[int64]bool)
|
||||
for _, include := range plugin.Includes {
|
||||
if include.Type != plugins.PluginTypeDashboard {
|
||||
if include.Type != plugins.TypeDashboard {
|
||||
continue
|
||||
}
|
||||
|
||||
dashboard, err := pm.LoadPluginDashboard(plugin.Id, include.Path)
|
||||
dashboard, err := m.LoadPluginDashboard(plugin.ID, include.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &plugins.PluginDashboardInfoDTO{}
|
||||
res.Path = include.Path
|
||||
res.PluginId = plugin.Id
|
||||
res.PluginId = plugin.ID
|
||||
res.Title = dashboard.Title
|
||||
res.Revision = dashboard.Data.Get("revision").MustInt64(1)
|
||||
|
||||
@@ -70,10 +71,10 @@ func (pm *PluginManager) GetPluginDashboards(orgID int64, pluginID string) ([]*p
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (pm *PluginManager) LoadPluginDashboard(pluginID, path string) (*models.Dashboard, error) {
|
||||
plugin := pm.GetPlugin(pluginID)
|
||||
func (m *PluginManager) LoadPluginDashboard(pluginID, path string) (*models.Dashboard, error) {
|
||||
plugin := m.Plugin(pluginID)
|
||||
if plugin == nil {
|
||||
return nil, plugins.PluginNotFoundError{PluginID: pluginID}
|
||||
return nil, plugins.NotFoundError{PluginID: pluginID}
|
||||
}
|
||||
|
||||
dashboardFilePath := filepath.Join(plugin.PluginDir, path)
|
||||
@@ -88,7 +89,7 @@ func (pm *PluginManager) LoadPluginDashboard(pluginID, path string) (*models.Das
|
||||
|
||||
defer func() {
|
||||
if err := reader.Close(); err != nil {
|
||||
plog.Warn("Failed to close file", "path", dashboardFilePath, "err", err)
|
||||
m.log.Warn("Failed to close file", "path", dashboardFilePath, "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -99,3 +100,62 @@ func (pm *PluginManager) LoadPluginDashboard(pluginID, path string) (*models.Das
|
||||
|
||||
return models.NewDashboardFromJson(data), nil
|
||||
}
|
||||
|
||||
func (m *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json,
|
||||
overwrite bool, inputs []plugins.ImportDashboardInput, user *models.SignedInUser) (plugins.PluginDashboardInfoDTO,
|
||||
*models.Dashboard, error) {
|
||||
var dashboard *models.Dashboard
|
||||
if pluginID != "" {
|
||||
var err error
|
||||
if dashboard, err = m.LoadPluginDashboard(pluginID, path); err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
} else {
|
||||
dashboard = models.NewDashboardFromJson(dashboardModel)
|
||||
}
|
||||
|
||||
evaluator := &DashTemplateEvaluator{
|
||||
template: dashboard.Data,
|
||||
inputs: inputs,
|
||||
}
|
||||
|
||||
generatedDash, err := evaluator.Eval()
|
||||
if err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
|
||||
saveCmd := models.SaveDashboardCommand{
|
||||
Dashboard: generatedDash,
|
||||
OrgId: orgID,
|
||||
UserId: user.UserId,
|
||||
Overwrite: overwrite,
|
||||
PluginId: pluginID,
|
||||
FolderId: folderID,
|
||||
}
|
||||
|
||||
dto := &dashboards.SaveDashboardDTO{
|
||||
OrgId: orgID,
|
||||
Dashboard: saveCmd.GetDashboardModel(),
|
||||
Overwrite: saveCmd.Overwrite,
|
||||
User: user,
|
||||
}
|
||||
|
||||
savedDash, err := dashboards.NewService(m.sqlStore).ImportDashboard(dto)
|
||||
if err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
|
||||
return plugins.PluginDashboardInfoDTO{
|
||||
PluginId: pluginID,
|
||||
Title: savedDash.Title,
|
||||
Path: path,
|
||||
Revision: savedDash.Data.Get("revision").MustInt64(1),
|
||||
FolderId: savedDash.FolderId,
|
||||
ImportedUri: "db/" + savedDash.Slug,
|
||||
ImportedUrl: savedDash.GetUrl(),
|
||||
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
|
||||
Imported: true,
|
||||
DashboardId: savedDash.Id,
|
||||
Slug: savedDash.Slug,
|
||||
}, savedDash, nil
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -22,7 +23,7 @@ func TestGetPluginDashboards(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
pm := newManager(cfg, &sqlstore.SQLStore{}, &fakeBackendPluginManager{})
|
||||
pm := newManager(cfg, nil, loader.New(nil, cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}), &sqlstore.SQLStore{})
|
||||
err := pm.init()
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
const (
|
||||
signatureMissing plugins.ErrorCode = "signatureMissing"
|
||||
signatureModified plugins.ErrorCode = "signatureModified"
|
||||
signatureInvalid plugins.ErrorCode = "signatureInvalid"
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
package installer
|
||||
|
||||
type Logger interface {
|
||||
Successf(format string, args ...interface{})
|
||||
Failuref(format string, args ...interface{})
|
||||
|
||||
Info(args ...interface{})
|
||||
Infof(format string, args ...interface{})
|
||||
Debug(args ...interface{})
|
||||
Debugf(format string, args ...interface{})
|
||||
Warn(args ...interface{})
|
||||
Warnf(format string, args ...interface{})
|
||||
Error(args ...interface{})
|
||||
Errorf(format string, args ...interface{})
|
||||
}
|
||||
@@ -33,7 +33,7 @@ type Installer struct {
|
||||
httpClient http.Client
|
||||
httpClientNoTimeout http.Client
|
||||
grafanaVersion string
|
||||
log plugins.PluginInstallerLogger
|
||||
log Logger
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -80,7 +80,7 @@ func (e ErrVersionNotFound) Error() string {
|
||||
return fmt.Sprintf("%s v%s either does not exist or is not supported on your system (%s)", e.PluginID, e.RequestedVersion, e.SystemInfo)
|
||||
}
|
||||
|
||||
func New(skipTLSVerify bool, grafanaVersion string, logger plugins.PluginInstallerLogger) *Installer {
|
||||
func New(skipTLSVerify bool, grafanaVersion string, logger Logger) plugins.Installer {
|
||||
return &Installer{
|
||||
httpClient: makeHttpClient(skipTLSVerify, 10*time.Second),
|
||||
httpClientNoTimeout: makeHttpClient(skipTLSVerify, 0),
|
||||
@@ -410,7 +410,7 @@ func normalizeVersion(version string) string {
|
||||
return normalized
|
||||
}
|
||||
|
||||
func (i *Installer) GetUpdateInfo(pluginID, version, pluginRepoURL string) (plugins.UpdateInfo, error) {
|
||||
func (i *Installer) GetUpdateInfo(ctx context.Context, pluginID, version, pluginRepoURL string) (plugins.UpdateInfo, error) {
|
||||
plugin, err := i.getPluginMetadataFromPluginRepo(pluginID, pluginRepoURL)
|
||||
if err != nil {
|
||||
return plugins.UpdateInfo{}, err
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package finder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var logger = log.New("plugin.finder")
|
||||
|
||||
type Finder struct {
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func New(cfg *setting.Cfg) Finder {
|
||||
return Finder{cfg: cfg}
|
||||
}
|
||||
|
||||
func (f *Finder) Find(pluginDirs []string) ([]string, error) {
|
||||
var pluginJSONPaths []string
|
||||
|
||||
for _, dir := range pluginDirs {
|
||||
exists, err := fs.Exists(dir)
|
||||
if err != nil {
|
||||
log.Warn("Error occurred when checking if plugin directory exists", "dir", dir, "err", err)
|
||||
}
|
||||
if !exists {
|
||||
logger.Warn("Skipping finding plugins as directory does not exist", "dir", dir)
|
||||
continue
|
||||
}
|
||||
|
||||
paths, err := f.getPluginJSONPaths(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pluginJSONPaths = append(pluginJSONPaths, paths...)
|
||||
}
|
||||
|
||||
return pluginJSONPaths, nil
|
||||
}
|
||||
|
||||
func (f *Finder) getPluginJSONPaths(dir string) ([]string, error) {
|
||||
var pluginJSONPaths []string
|
||||
|
||||
var err error
|
||||
dir, err = filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
if err := util.Walk(dir, true, true,
|
||||
func(currentPath string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("filepath.Walk reported an error for %q: %w", currentPath, err)
|
||||
}
|
||||
|
||||
if fi.Name() == "node_modules" {
|
||||
return util.ErrWalkSkipDir
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.Name() != "plugin.json" {
|
||||
return nil
|
||||
}
|
||||
|
||||
pluginJSONPaths = append(pluginJSONPaths, currentPath)
|
||||
return nil
|
||||
}); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
logger.Debug("Couldn't scan directory since it doesn't exist", "pluginDir", dir, "err", err)
|
||||
return []string{}, nil
|
||||
}
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
logger.Debug("Couldn't scan directory due to lack of permissions", "pluginDir", dir, "err", err)
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
return pluginJSONPaths, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package finder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestFinder_Find(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
cfg *setting.Cfg
|
||||
pluginDirs []string
|
||||
expectedPathSuffix []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "Dir with single plugin",
|
||||
cfg: setting.NewCfg(),
|
||||
pluginDirs: []string{"../../testdata/valid-v2-signature"},
|
||||
expectedPathSuffix: []string{"/pkg/plugins/manager/testdata/valid-v2-signature/plugin/plugin.json"},
|
||||
},
|
||||
{
|
||||
name: "Dir with nested plugins",
|
||||
cfg: setting.NewCfg(),
|
||||
pluginDirs: []string{"../../testdata/duplicate-plugins"},
|
||||
expectedPathSuffix: []string{
|
||||
"/pkg/plugins/manager/testdata/duplicate-plugins/nested/nested/plugin.json",
|
||||
"/pkg/plugins/manager/testdata/duplicate-plugins/nested/plugin.json",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Dir with single plugin which has symbolic link root directory",
|
||||
cfg: setting.NewCfg(),
|
||||
pluginDirs: []string{"../../testdata/symbolic-plugin-dirs"},
|
||||
expectedPathSuffix: []string{"/pkg/plugins/manager/testdata/includes-symlinks/plugin.json"},
|
||||
},
|
||||
{
|
||||
name: "Multiple plugin dirs",
|
||||
cfg: setting.NewCfg(),
|
||||
pluginDirs: []string{"../../testdata/duplicate-plugins", "../../testdata/invalid-v1-signature"},
|
||||
expectedPathSuffix: []string{
|
||||
"/pkg/plugins/manager/testdata/duplicate-plugins/nested/nested/plugin.json",
|
||||
"/pkg/plugins/manager/testdata/duplicate-plugins/nested/plugin.json",
|
||||
"/pkg/plugins/manager/testdata/invalid-v1-signature/plugin/plugin.json"},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f := New(tc.cfg)
|
||||
pluginPaths, err := f.Find(tc.pluginDirs)
|
||||
if (err != nil) && !errors.Is(err, tc.err) {
|
||||
t.Errorf("Find() error = %v, expected error %v", err, tc.err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, len(tc.expectedPathSuffix), len(pluginPaths))
|
||||
for i := 0; i < len(tc.expectedPathSuffix); i++ {
|
||||
assert.True(t, strings.HasSuffix(pluginPaths[i], tc.expectedPathSuffix[i]))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package initializer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/grpcplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var logger = log.New("plugin.initializer")
|
||||
|
||||
type Initializer struct {
|
||||
cfg *setting.Cfg
|
||||
license models.Licensing
|
||||
}
|
||||
|
||||
func New(cfg *setting.Cfg, license models.Licensing) Initializer {
|
||||
return Initializer{
|
||||
cfg: cfg,
|
||||
license: license,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Initializer) Initialize(p *plugins.Plugin) error {
|
||||
if len(p.Dependencies.Plugins) == 0 {
|
||||
p.Dependencies.Plugins = []plugins.Dependency{}
|
||||
}
|
||||
|
||||
if p.Dependencies.GrafanaVersion == "" {
|
||||
p.Dependencies.GrafanaVersion = "*"
|
||||
}
|
||||
|
||||
for _, include := range p.Includes {
|
||||
if include.Role == "" {
|
||||
include.Role = models.ROLE_VIEWER
|
||||
}
|
||||
}
|
||||
|
||||
i.handleModuleDefaults(p)
|
||||
|
||||
p.Info.Logos.Small = pluginLogoURL(p.Type, p.Info.Logos.Small, p.BaseURL)
|
||||
p.Info.Logos.Large = pluginLogoURL(p.Type, p.Info.Logos.Large, p.BaseURL)
|
||||
|
||||
for i := 0; i < len(p.Info.Screenshots); i++ {
|
||||
p.Info.Screenshots[i].Path = evalRelativePluginURLPath(p.Info.Screenshots[i].Path, p.BaseURL, p.Type)
|
||||
}
|
||||
|
||||
if p.IsApp() {
|
||||
for _, child := range p.Children {
|
||||
i.setPathsBasedOnApp(p, child)
|
||||
}
|
||||
|
||||
// slugify pages
|
||||
for _, include := range p.Includes {
|
||||
if include.Slug == "" {
|
||||
include.Slug = slug.Make(include.Name)
|
||||
}
|
||||
if include.Type == "page" && include.DefaultNav {
|
||||
p.DefaultNavURL = i.cfg.AppSubURL + "/plugins/" + p.ID + "/page/" + include.Slug
|
||||
}
|
||||
if include.Type == "dashboard" && include.DefaultNav {
|
||||
p.DefaultNavURL = i.cfg.AppSubURL + "/dashboard/db/" + include.Slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluginLog := logger.New("pluginID", p.ID)
|
||||
p.SetLogger(pluginLog)
|
||||
|
||||
if p.Backend {
|
||||
var backendFactory backendplugin.PluginFactoryFunc
|
||||
if p.IsRenderer() {
|
||||
cmd := plugins.ComposeRendererStartCommand()
|
||||
backendFactory = grpcplugin.NewRendererPlugin(p.ID, filepath.Join(p.PluginDir, cmd),
|
||||
func(pluginID string, renderer pluginextensionv2.RendererPlugin, logger log.Logger) error {
|
||||
p.Renderer = renderer
|
||||
return nil
|
||||
},
|
||||
)
|
||||
} else {
|
||||
cmd := plugins.ComposePluginStartCommand(p.Executable)
|
||||
backendFactory = grpcplugin.NewBackendPlugin(p.ID, filepath.Join(p.PluginDir, cmd))
|
||||
}
|
||||
|
||||
if backendClient, err := backendFactory(p.ID, pluginLog, i.envVars(p)); err != nil {
|
||||
return err
|
||||
} else {
|
||||
p.RegisterClient(backendClient)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Initializer) InitializeWithFactory(p *plugins.Plugin, factory backendplugin.PluginFactoryFunc) error {
|
||||
err := i.Initialize(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if factory != nil {
|
||||
var err error
|
||||
|
||||
f, err := factory(p.ID, log.New("pluginID", p.ID), []string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.RegisterClient(f)
|
||||
} else {
|
||||
logger.Warn("Could not initialize core plugin process", "pluginID", p.ID)
|
||||
return fmt.Errorf("could not initialize plugin %s", p.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Initializer) handleModuleDefaults(p *plugins.Plugin) {
|
||||
if p.IsCorePlugin() {
|
||||
// Previously there was an assumption that the Core plugins directory
|
||||
// should be public/app/plugins/<plugin type>/<plugin id>
|
||||
// However this can be an issue if the Core plugins directory is renamed
|
||||
baseDir := filepath.Base(p.PluginDir)
|
||||
|
||||
// use path package for the following statements because these are not file paths
|
||||
p.Module = path.Join("app/plugins", string(p.Type), baseDir, "module")
|
||||
p.BaseURL = path.Join("public/app/plugins", string(p.Type), baseDir)
|
||||
return
|
||||
}
|
||||
|
||||
metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature))
|
||||
|
||||
p.Module = path.Join("plugins", p.ID, "module")
|
||||
p.BaseURL = path.Join("public/plugins", p.ID)
|
||||
}
|
||||
|
||||
func (i *Initializer) setPathsBasedOnApp(parent *plugins.Plugin, child *plugins.Plugin) {
|
||||
appSubPath := strings.ReplaceAll(strings.Replace(child.PluginDir, parent.PluginDir, "", 1), "\\", "/")
|
||||
child.IncludedInAppID = parent.ID
|
||||
child.BaseURL = parent.BaseURL
|
||||
|
||||
if parent.IsCorePlugin() {
|
||||
child.Module = util.JoinURLFragments("app/plugins/app/"+parent.ID, appSubPath) + "/module"
|
||||
} else {
|
||||
child.Module = util.JoinURLFragments("plugins/"+parent.ID, appSubPath) + "/module"
|
||||
}
|
||||
}
|
||||
|
||||
func pluginLogoURL(pluginType plugins.Type, path, baseURL string) string {
|
||||
if path == "" {
|
||||
return defaultLogoPath(pluginType)
|
||||
}
|
||||
|
||||
return evalRelativePluginURLPath(path, baseURL, pluginType)
|
||||
}
|
||||
|
||||
func defaultLogoPath(pluginType plugins.Type) string {
|
||||
return "public/img/icn-" + string(pluginType) + ".svg"
|
||||
}
|
||||
|
||||
func evalRelativePluginURLPath(pathStr, baseURL string, pluginType plugins.Type) string {
|
||||
if pathStr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
u, _ := url.Parse(pathStr)
|
||||
if u.IsAbs() {
|
||||
return pathStr
|
||||
}
|
||||
|
||||
// is set as default or has already been prefixed with base path
|
||||
if pathStr == defaultLogoPath(pluginType) || strings.HasPrefix(pathStr, baseURL) {
|
||||
return pathStr
|
||||
}
|
||||
|
||||
return path.Join(baseURL, pathStr)
|
||||
}
|
||||
|
||||
func (i *Initializer) envVars(plugin *plugins.Plugin) []string {
|
||||
hostEnv := []string{
|
||||
fmt.Sprintf("GF_VERSION=%s", i.cfg.BuildVersion),
|
||||
}
|
||||
|
||||
if i.license != nil && i.license.HasLicense() {
|
||||
hostEnv = append(
|
||||
hostEnv,
|
||||
fmt.Sprintf("GF_EDITION=%s", i.license.Edition()),
|
||||
fmt.Sprintf("GF_ENTERPRISE_license_PATH=%s", i.cfg.EnterpriseLicensePath),
|
||||
)
|
||||
|
||||
if envProvider, ok := i.license.(models.LicenseEnvironment); ok {
|
||||
for k, v := range envProvider.Environment() {
|
||||
hostEnv = append(hostEnv, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hostEnv = append(hostEnv, i.awsEnvVars()...)
|
||||
hostEnv = append(hostEnv, i.azureEnvVars()...)
|
||||
return getPluginSettings(plugin.ID, i.cfg).asEnvVar("GF_PLUGIN", hostEnv)
|
||||
}
|
||||
|
||||
func (i *Initializer) awsEnvVars() []string {
|
||||
var variables []string
|
||||
if i.cfg.AWSAssumeRoleEnabled {
|
||||
variables = append(variables, awsds.AssumeRoleEnabledEnvVarKeyName+"=true")
|
||||
}
|
||||
if len(i.cfg.AWSAllowedAuthProviders) > 0 {
|
||||
variables = append(variables, awsds.AllowedAuthProvidersEnvVarKeyName+"="+strings.Join(i.cfg.AWSAllowedAuthProviders, ","))
|
||||
}
|
||||
|
||||
return variables
|
||||
}
|
||||
|
||||
func (i *Initializer) azureEnvVars() []string {
|
||||
var variables []string
|
||||
if i.cfg.Azure.Cloud != "" {
|
||||
variables = append(variables, "AZURE_CLOUD="+i.cfg.Azure.Cloud)
|
||||
}
|
||||
if i.cfg.Azure.ManagedIdentityClientId != "" {
|
||||
variables = append(variables, "AZURE_MANAGED_IDENTITY_CLIENT_ID="+i.cfg.Azure.ManagedIdentityClientId)
|
||||
}
|
||||
if i.cfg.Azure.ManagedIdentityEnabled {
|
||||
variables = append(variables, "AZURE_MANAGED_IDENTITY_ENABLED=true")
|
||||
}
|
||||
|
||||
return variables
|
||||
}
|
||||
|
||||
type pluginSettings map[string]string
|
||||
|
||||
func (ps pluginSettings) asEnvVar(prefix string, hostEnv []string) []string {
|
||||
var env []string
|
||||
for k, v := range ps {
|
||||
key := fmt.Sprintf("%s_%s", prefix, strings.ToUpper(k))
|
||||
if value := os.Getenv(key); value != "" {
|
||||
v = value
|
||||
}
|
||||
|
||||
env = append(env, fmt.Sprintf("%s=%s", key, v))
|
||||
}
|
||||
|
||||
env = append(env, hostEnv...)
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
func getPluginSettings(pluginID string, cfg *setting.Cfg) pluginSettings {
|
||||
ps := pluginSettings{}
|
||||
for k, v := range cfg.PluginSettings[pluginID] {
|
||||
if k == "path" || strings.ToLower(k) == "id" {
|
||||
continue
|
||||
}
|
||||
ps[k] = v
|
||||
}
|
||||
|
||||
return ps
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package initializer
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestInitializer_Initialize(t *testing.T) {
|
||||
absCurPath, err := filepath.Abs(".")
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Run("core backend datasource", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: plugins.DataSource,
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Name: "Example dashboard",
|
||||
Type: plugins.TypeDashboard,
|
||||
},
|
||||
},
|
||||
Backend: true,
|
||||
},
|
||||
PluginDir: absCurPath,
|
||||
Class: plugins.Core,
|
||||
}
|
||||
|
||||
i := &Initializer{
|
||||
cfg: setting.NewCfg(),
|
||||
}
|
||||
|
||||
err := i.Initialize(p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "public/img/icn-datasource.svg", p.Info.Logos.Small)
|
||||
assert.Equal(t, "public/img/icn-datasource.svg", p.Info.Logos.Large)
|
||||
assert.Equal(t, "*", p.Dependencies.GrafanaVersion)
|
||||
assert.Len(t, p.Includes, 1)
|
||||
assert.Equal(t, models.ROLE_VIEWER, p.Includes[0].Role)
|
||||
assert.Equal(t, filepath.Join("app/plugins/datasource", filepath.Base(p.PluginDir), "module"), p.Module)
|
||||
assert.Equal(t, path.Join("public/app/plugins/datasource", filepath.Base(p.PluginDir)), p.BaseURL)
|
||||
assert.NotNil(t, p.Logger())
|
||||
c, exists := p.Client()
|
||||
assert.True(t, exists)
|
||||
assert.NotNil(t, c)
|
||||
})
|
||||
|
||||
t.Run("renderer", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: plugins.Renderer,
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: ">=8.x",
|
||||
},
|
||||
Backend: true,
|
||||
},
|
||||
PluginDir: absCurPath,
|
||||
Class: plugins.External,
|
||||
}
|
||||
|
||||
i := &Initializer{
|
||||
cfg: setting.NewCfg(),
|
||||
}
|
||||
|
||||
err := i.Initialize(p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// TODO add default img to project
|
||||
assert.Equal(t, "public/img/icn-renderer.svg", p.Info.Logos.Small)
|
||||
assert.Equal(t, "public/img/icn-renderer.svg", p.Info.Logos.Large)
|
||||
assert.Equal(t, ">=8.x", p.Dependencies.GrafanaVersion)
|
||||
assert.Equal(t, "plugins/test/module", p.Module)
|
||||
assert.Equal(t, "public/plugins/test", p.BaseURL)
|
||||
assert.NotNil(t, p.Logger())
|
||||
c, exists := p.Client()
|
||||
assert.True(t, exists)
|
||||
assert.NotNil(t, c)
|
||||
})
|
||||
|
||||
t.Run("external app", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "parent-plugin",
|
||||
Type: plugins.App,
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Type: "page",
|
||||
DefaultNav: true,
|
||||
Slug: "myCustomSlug",
|
||||
},
|
||||
},
|
||||
},
|
||||
PluginDir: absCurPath,
|
||||
Class: plugins.External,
|
||||
Children: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "child-plugin",
|
||||
},
|
||||
PluginDir: absCurPath,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
i := &Initializer{
|
||||
cfg: &setting.Cfg{
|
||||
AppSubURL: "appSubURL",
|
||||
},
|
||||
}
|
||||
|
||||
err := i.Initialize(p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "public/img/icn-app.svg", p.Info.Logos.Small)
|
||||
assert.Equal(t, "public/img/icn-app.svg", p.Info.Logos.Large)
|
||||
assert.Equal(t, "*", p.Dependencies.GrafanaVersion)
|
||||
assert.Len(t, p.Includes, 1)
|
||||
assert.Equal(t, models.ROLE_VIEWER, p.Includes[0].Role)
|
||||
assert.Equal(t, filepath.Join("plugins", p.ID, "module"), p.Module)
|
||||
assert.Equal(t, "public/plugins/parent-plugin", p.BaseURL)
|
||||
assert.NotNil(t, p.Logger())
|
||||
c, exists := p.Client()
|
||||
assert.False(t, exists)
|
||||
assert.Nil(t, c)
|
||||
|
||||
assert.Len(t, p.Children, 1)
|
||||
assert.Equal(t, p.ID, p.Children[0].IncludedInAppID)
|
||||
assert.Equal(t, "public/plugins/parent-plugin", p.Children[0].BaseURL)
|
||||
assert.Equal(t, "plugins/parent-plugin/module", p.Children[0].Module)
|
||||
assert.Equal(t, "appSubURL/plugins/parent-plugin/page/myCustomSlug", p.DefaultNavURL)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInitializer_InitializeWithFactory(t *testing.T) {
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-plugin",
|
||||
Type: plugins.App,
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Type: "page",
|
||||
DefaultNav: true,
|
||||
Slug: "myCustomSlug",
|
||||
},
|
||||
},
|
||||
},
|
||||
PluginDir: "test/folder",
|
||||
Class: plugins.External,
|
||||
}
|
||||
i := &Initializer{
|
||||
cfg: &setting.Cfg{
|
||||
AppSubURL: "appSubURL",
|
||||
},
|
||||
}
|
||||
|
||||
factoryInvoked := false
|
||||
|
||||
factory := backendplugin.PluginFactoryFunc(func(pluginID string, logger log.Logger, env []string) (backendplugin.Plugin, error) {
|
||||
factoryInvoked = true
|
||||
return testPlugin{}, nil
|
||||
})
|
||||
|
||||
err := i.InitializeWithFactory(p, factory)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.True(t, factoryInvoked)
|
||||
assert.NotNil(t, p.Logger())
|
||||
client, exists := p.Client()
|
||||
assert.True(t, exists)
|
||||
assert.NotNil(t, client.(testPlugin))
|
||||
})
|
||||
|
||||
t.Run("invalid factory", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-plugin",
|
||||
Type: plugins.App,
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Type: "page",
|
||||
DefaultNav: true,
|
||||
Slug: "myCustomSlug",
|
||||
},
|
||||
},
|
||||
},
|
||||
PluginDir: "test/folder",
|
||||
Class: plugins.External,
|
||||
}
|
||||
i := &Initializer{
|
||||
cfg: &setting.Cfg{
|
||||
AppSubURL: "appSubURL",
|
||||
},
|
||||
}
|
||||
|
||||
err := i.InitializeWithFactory(p, nil)
|
||||
assert.Errorf(t, err, "could not initialize plugin test-plugin")
|
||||
|
||||
c, exists := p.Client()
|
||||
assert.False(t, exists)
|
||||
assert.Nil(t, c)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInitializer_envVars(t *testing.T) {
|
||||
t.Run("backend datasource with license", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
},
|
||||
}
|
||||
|
||||
licensing := &testLicensingService{
|
||||
edition: "test",
|
||||
hasLicense: true,
|
||||
}
|
||||
|
||||
i := &Initializer{
|
||||
cfg: &setting.Cfg{
|
||||
EnterpriseLicensePath: "/path/to/ent/license",
|
||||
PluginSettings: map[string]map[string]string{
|
||||
"test": {
|
||||
"custom_env_var": "customVal",
|
||||
},
|
||||
},
|
||||
},
|
||||
license: licensing,
|
||||
}
|
||||
|
||||
envVars := i.envVars(p)
|
||||
assert.Len(t, envVars, 5)
|
||||
assert.Equal(t, "GF_PLUGIN_CUSTOM_ENV_VAR=customVal", envVars[0])
|
||||
assert.Equal(t, "GF_VERSION=", envVars[1])
|
||||
assert.Equal(t, "GF_EDITION=test", envVars[2])
|
||||
assert.Equal(t, "GF_ENTERPRISE_license_PATH=/path/to/ent/license", envVars[3])
|
||||
assert.Equal(t, "GF_ENTERPRISE_LICENSE_TEXT=", envVars[4])
|
||||
})
|
||||
}
|
||||
|
||||
func TestInitializer_setPathsBasedOnApp(t *testing.T) {
|
||||
t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) {
|
||||
i := &Initializer{
|
||||
cfg: setting.NewCfg(),
|
||||
}
|
||||
|
||||
child := &plugins.Plugin{
|
||||
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata\\datasources\\datasource",
|
||||
}
|
||||
parent := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "testdata",
|
||||
},
|
||||
Class: plugins.Core,
|
||||
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata",
|
||||
BaseURL: "public/app/plugins/app/testdata",
|
||||
}
|
||||
|
||||
i.setPathsBasedOnApp(parent, child)
|
||||
|
||||
assert.Equal(t, "app/plugins/app/testdata/datasources/datasource/module", child.Module)
|
||||
assert.Equal(t, "testdata", child.IncludedInAppID)
|
||||
assert.Equal(t, "public/app/plugins/app/testdata", child.BaseURL)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInitializer_getAWSEnvironmentVariables(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestInitializer_getAzureEnvironmentVariables(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestInitializer_handleModuleDefaults(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func Test_defaultLogoPath(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func Test_evalRelativePluginUrlPath(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func Test_getPluginLogoUrl(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func Test_getPluginSettings(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func Test_pluginSettings_ToEnv(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
type testLicensingService struct {
|
||||
edition string
|
||||
hasLicense bool
|
||||
tokenRaw string
|
||||
}
|
||||
|
||||
func (t *testLicensingService) HasLicense() bool {
|
||||
return t.hasLicense
|
||||
}
|
||||
|
||||
func (t *testLicensingService) Expiry() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (t *testLicensingService) Edition() string {
|
||||
return t.edition
|
||||
}
|
||||
|
||||
func (t *testLicensingService) StateInfo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *testLicensingService) ContentDeliveryPrefix() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *testLicensingService) LicenseURL(showAdminLicensingPage bool) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *testLicensingService) HasValidLicense() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *testLicensingService) Environment() map[string]string {
|
||||
return map[string]string{"GF_ENTERPRISE_LICENSE_TEXT": t.tokenRaw}
|
||||
}
|
||||
|
||||
type testPlugin struct {
|
||||
backendplugin.Plugin
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = log.New("plugin.loader")
|
||||
ErrInvalidPluginJSON = errors.New("did not find valid type or id properties in plugin.json")
|
||||
ErrInvalidPluginJSONFilePath = errors.New("invalid plugin.json filepath was provided")
|
||||
)
|
||||
|
||||
var _ plugins.ErrorResolver = (*Loader)(nil)
|
||||
|
||||
type Loader struct {
|
||||
cfg *setting.Cfg
|
||||
pluginFinder finder.Finder
|
||||
pluginInitializer initializer.Initializer
|
||||
signatureValidator signature.Validator
|
||||
|
||||
errs map[string]*plugins.SignatureError
|
||||
}
|
||||
|
||||
func ProvideService(license models.Licensing, cfg *setting.Cfg, authorizer plugins.PluginLoaderAuthorizer) (*Loader, error) {
|
||||
return New(license, cfg, authorizer), nil
|
||||
}
|
||||
|
||||
func New(license models.Licensing, cfg *setting.Cfg, authorizer plugins.PluginLoaderAuthorizer) *Loader {
|
||||
return &Loader{
|
||||
cfg: cfg,
|
||||
pluginFinder: finder.New(cfg),
|
||||
pluginInitializer: initializer.New(cfg, license),
|
||||
signatureValidator: signature.NewValidator(cfg, authorizer),
|
||||
errs: make(map[string]*plugins.SignatureError),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Loader) Load(paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) {
|
||||
pluginJSONPaths, err := l.pluginFinder.Find(paths)
|
||||
if err != nil {
|
||||
logger.Error("plugin finder encountered an error", "err", err)
|
||||
}
|
||||
|
||||
return l.loadPlugins(pluginJSONPaths, ignore)
|
||||
}
|
||||
|
||||
func (l *Loader) LoadWithFactory(path string, factory backendplugin.PluginFactoryFunc) (*plugins.Plugin, error) {
|
||||
p, err := l.load(path, map[string]struct{}{})
|
||||
if err != nil {
|
||||
logger.Error("failed to load core plugin", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = l.pluginInitializer.InitializeWithFactory(p, factory)
|
||||
|
||||
return p, err
|
||||
}
|
||||
|
||||
func (l *Loader) load(path string, ignore map[string]struct{}) (*plugins.Plugin, error) {
|
||||
pluginJSONPaths, err := l.pluginFinder.Find([]string{path})
|
||||
if err != nil {
|
||||
logger.Error("failed to find plugin", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loadedPlugins, err := l.loadPlugins(pluginJSONPaths, ignore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(loadedPlugins) == 0 {
|
||||
return nil, fmt.Errorf("could not load plugin at path %s", path)
|
||||
}
|
||||
|
||||
return loadedPlugins[0], nil
|
||||
}
|
||||
|
||||
func (l *Loader) loadPlugins(pluginJSONPaths []string, existingPlugins map[string]struct{}) ([]*plugins.Plugin, error) {
|
||||
var foundPlugins = foundPlugins{}
|
||||
|
||||
// load plugin.json files and map directory to JSON data
|
||||
for _, pluginJSONPath := range pluginJSONPaths {
|
||||
plugin, err := l.readPluginJSON(pluginJSONPath)
|
||||
if err != nil {
|
||||
logger.Warn("Skipping plugin loading as it's plugin.json is invalid", "id", plugin.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
pluginJSONAbsPath, err := filepath.Abs(pluginJSONPath)
|
||||
if err != nil {
|
||||
logger.Warn("Skipping plugin loading as full plugin.json path could not be calculated", "id", plugin.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, dupe := foundPlugins[filepath.Dir(pluginJSONAbsPath)]; dupe {
|
||||
logger.Warn("Skipping plugin loading as it's a duplicate", "id", plugin.ID)
|
||||
continue
|
||||
}
|
||||
foundPlugins[filepath.Dir(pluginJSONAbsPath)] = plugin
|
||||
}
|
||||
|
||||
foundPlugins.stripDuplicates(existingPlugins)
|
||||
|
||||
// calculate initial signature state
|
||||
loadedPlugins := make(map[string]*plugins.Plugin)
|
||||
for pluginDir, pluginJSON := range foundPlugins {
|
||||
plugin := &plugins.Plugin{
|
||||
JSONData: pluginJSON,
|
||||
PluginDir: pluginDir,
|
||||
Class: l.pluginClass(pluginDir),
|
||||
}
|
||||
|
||||
sig, err := signature.Calculate(logger, plugin)
|
||||
if err != nil {
|
||||
logger.Warn("Could not calculate plugin signature state", "pluginID", plugin.ID, "err", err)
|
||||
continue
|
||||
}
|
||||
plugin.Signature = sig.Status
|
||||
plugin.SignatureType = sig.Type
|
||||
plugin.SignatureOrg = sig.SigningOrg
|
||||
plugin.SignedFiles = sig.Files
|
||||
|
||||
loadedPlugins[plugin.PluginDir] = plugin
|
||||
}
|
||||
|
||||
// wire up plugin dependencies
|
||||
for _, plugin := range loadedPlugins {
|
||||
ancestors := strings.Split(plugin.PluginDir, string(filepath.Separator))
|
||||
ancestors = ancestors[0 : len(ancestors)-1]
|
||||
pluginPath := ""
|
||||
|
||||
if runtime.GOOS != "windows" && filepath.IsAbs(plugin.PluginDir) {
|
||||
pluginPath = "/"
|
||||
}
|
||||
for _, ancestor := range ancestors {
|
||||
pluginPath = filepath.Join(pluginPath, ancestor)
|
||||
if parentPlugin, ok := loadedPlugins[pluginPath]; ok {
|
||||
plugin.Parent = parentPlugin
|
||||
plugin.Parent.Children = append(plugin.Parent.Children, plugin)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validate signatures
|
||||
verifiedPlugins := []*plugins.Plugin{}
|
||||
for _, plugin := range loadedPlugins {
|
||||
signingError := l.signatureValidator.Validate(plugin)
|
||||
if signingError != nil {
|
||||
logger.Warn("Skipping loading plugin due to problem with signature",
|
||||
"pluginID", plugin.ID, "status", signingError.SignatureStatus)
|
||||
plugin.SignatureError = signingError
|
||||
l.errs[plugin.ID] = signingError
|
||||
// skip plugin so it will not be loaded any further
|
||||
continue
|
||||
}
|
||||
|
||||
// clear plugin error if a pre-existing error has since been resolved
|
||||
delete(l.errs, plugin.ID)
|
||||
|
||||
// verify module.js exists for SystemJS to load
|
||||
if !plugin.IsRenderer() && !plugin.IsCorePlugin() {
|
||||
module := filepath.Join(plugin.PluginDir, "module.js")
|
||||
if exists, err := fs.Exists(module); err != nil {
|
||||
return nil, err
|
||||
} else if !exists {
|
||||
logger.Warn("Plugin missing module.js",
|
||||
"pluginID", plugin.ID,
|
||||
"warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.",
|
||||
"path", module)
|
||||
}
|
||||
}
|
||||
|
||||
verifiedPlugins = append(verifiedPlugins, plugin)
|
||||
}
|
||||
|
||||
for _, p := range verifiedPlugins {
|
||||
err := l.pluginInitializer.Initialize(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return verifiedPlugins, nil
|
||||
}
|
||||
|
||||
func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) {
|
||||
logger.Debug("Loading plugin", "path", pluginJSONPath)
|
||||
|
||||
if !strings.EqualFold(filepath.Ext(pluginJSONPath), ".json") {
|
||||
return plugins.JSONData{}, ErrInvalidPluginJSONFilePath
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `currentPath` is based
|
||||
// on plugin the folder structure on disk and not user input.
|
||||
reader, err := os.Open(pluginJSONPath)
|
||||
if err != nil {
|
||||
return plugins.JSONData{}, err
|
||||
}
|
||||
|
||||
plugin := plugins.JSONData{}
|
||||
if err := json.NewDecoder(reader).Decode(&plugin); err != nil {
|
||||
return plugins.JSONData{}, err
|
||||
}
|
||||
|
||||
if err := reader.Close(); err != nil {
|
||||
logger.Warn("Failed to close JSON file", "path", pluginJSONPath, "err", err)
|
||||
}
|
||||
|
||||
if err := validatePluginJSON(plugin); err != nil {
|
||||
return plugins.JSONData{}, err
|
||||
}
|
||||
|
||||
if plugin.ID == "grafana-piechart-panel" {
|
||||
plugin.Name = "Pie Chart (old)"
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
func (l *Loader) PluginErrors() []*plugins.Error {
|
||||
errs := make([]*plugins.Error, 0)
|
||||
for _, err := range l.errs {
|
||||
errs = append(errs, &plugins.Error{
|
||||
PluginID: err.PluginID,
|
||||
ErrorCode: err.AsErrorCode(),
|
||||
})
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func validatePluginJSON(data plugins.JSONData) error {
|
||||
if data.ID == "" || !data.Type.IsValid() {
|
||||
return ErrInvalidPluginJSON
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Loader) pluginClass(pluginDir string) plugins.Class {
|
||||
isSubDir := func(base, target string) bool {
|
||||
path, err := filepath.Rel(base, target)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(path, "..") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
corePluginsDir := filepath.Join(l.cfg.StaticRootPath, "app/plugins")
|
||||
if isSubDir(corePluginsDir, pluginDir) {
|
||||
return plugins.Core
|
||||
}
|
||||
|
||||
if isSubDir(l.cfg.BundledPluginsPath, pluginDir) {
|
||||
return plugins.Bundled
|
||||
}
|
||||
|
||||
return plugins.External
|
||||
}
|
||||
|
||||
type foundPlugins map[string]plugins.JSONData
|
||||
|
||||
// stripDuplicates will strip duplicate plugins or plugins that already exist
|
||||
func (f *foundPlugins) stripDuplicates(existingPlugins map[string]struct{}) {
|
||||
pluginsByID := make(map[string]struct{})
|
||||
for path, scannedPlugin := range *f {
|
||||
if _, existing := existingPlugins[scannedPlugin.ID]; existing {
|
||||
logger.Debug("Skipping plugin as it's already installed", "plugin", scannedPlugin.ID)
|
||||
delete(*f, path)
|
||||
continue
|
||||
}
|
||||
|
||||
pluginsByID[scannedPlugin.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,928 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var compareOpts = cmpopts.IgnoreFields(plugins.Plugin{}, "client", "log")
|
||||
|
||||
func TestLoader_Load(t *testing.T) {
|
||||
corePluginDir, err := filepath.Abs("./../../../../public")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of core plugins dir")
|
||||
return
|
||||
}
|
||||
parentDir, err := filepath.Abs("../")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of current dir")
|
||||
return
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *setting.Cfg
|
||||
pluginPaths []string
|
||||
existingPlugins map[string]struct{}
|
||||
want []*plugins.Plugin
|
||||
pluginErrors map[string]*plugins.Error
|
||||
}{
|
||||
{
|
||||
name: "Load a Core plugin",
|
||||
cfg: &setting.Cfg{
|
||||
StaticRootPath: corePluginDir,
|
||||
},
|
||||
pluginPaths: []string{filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch")},
|
||||
want: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "cloudwatch",
|
||||
Type: "datasource",
|
||||
Name: "CloudWatch",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Grafana Labs",
|
||||
URL: "https://grafana.com",
|
||||
},
|
||||
Description: "Data source for Amazon AWS monitoring service",
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
|
||||
Large: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{Name: "EC2", Path: "dashboards/ec2.json", Type: "dashboard", Role: "Viewer"},
|
||||
{Name: "EBS", Path: "dashboards/EBS.json", Type: "dashboard", Role: "Viewer"},
|
||||
{Name: "Lambda", Path: "dashboards/Lambda.json", Type: "dashboard", Role: "Viewer"},
|
||||
{Name: "Logs", Path: "dashboards/Logs.json", Type: "dashboard", Role: "Viewer"},
|
||||
{Name: "RDS", Path: "dashboards/RDS.json", Type: "dashboard", Role: "Viewer"},
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
Category: "cloud",
|
||||
Annotations: true,
|
||||
Metrics: true,
|
||||
Alerting: true,
|
||||
Logs: true,
|
||||
QueryOptions: map[string]bool{"minInterval": true},
|
||||
},
|
||||
Module: "app/plugins/datasource/cloudwatch/module",
|
||||
BaseURL: "public/app/plugins/datasource/cloudwatch",
|
||||
PluginDir: filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch"),
|
||||
Signature: "internal",
|
||||
Class: "core",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Load a Bundled plugin",
|
||||
cfg: &setting.Cfg{
|
||||
BundledPluginsPath: filepath.Join(parentDir, "testdata"),
|
||||
},
|
||||
pluginPaths: []string{"../testdata/valid-v2-signature"},
|
||||
want: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: "datasource",
|
||||
Name: "Test",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Will Browne",
|
||||
URL: "https://willbrowne.com",
|
||||
},
|
||||
Version: "1.0.0",
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-datasource.svg",
|
||||
Large: "public/img/icn-datasource.svg",
|
||||
},
|
||||
Description: "Test",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
Executable: "test",
|
||||
Backend: true,
|
||||
State: "alpha",
|
||||
},
|
||||
Module: "plugins/test/module",
|
||||
BaseURL: "public/plugins/test",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/valid-v2-signature/plugin/"),
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
Class: "bundled",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "Load an External plugin",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
},
|
||||
pluginPaths: []string{"../testdata/symbolic-plugin-dirs"},
|
||||
want: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-app",
|
||||
Type: "app",
|
||||
Name: "Test App",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Test Inc.",
|
||||
URL: "http://test.com",
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/plugins/test-app/img/logo_small.png",
|
||||
Large: "public/plugins/test-app/img/logo_large.png",
|
||||
},
|
||||
Links: []plugins.InfoLink{
|
||||
{Name: "Project site", URL: "http://project.com"},
|
||||
{Name: "License & Terms", URL: "http://license.com"},
|
||||
},
|
||||
Description: "Official Grafana Test App & Dashboard bundle",
|
||||
Screenshots: []plugins.Screenshots{
|
||||
{Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"},
|
||||
{Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"},
|
||||
},
|
||||
Version: "1.0.0",
|
||||
Updated: "2015-02-10",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "3.x.x",
|
||||
Plugins: []plugins.Dependency{
|
||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Name: "Nginx Connections",
|
||||
Path: "dashboards/connections.json",
|
||||
Type: "dashboard",
|
||||
Role: "Viewer",
|
||||
Slug: "nginx-connections",
|
||||
},
|
||||
{
|
||||
Name: "Nginx Memory",
|
||||
Path: "dashboards/memory.json",
|
||||
Type: "dashboard",
|
||||
Role: "Viewer",
|
||||
Slug: "nginx-memory",
|
||||
},
|
||||
{
|
||||
Name: "Nginx Panel",
|
||||
Type: "panel",
|
||||
Role: "Viewer",
|
||||
Slug: "nginx-panel"},
|
||||
{
|
||||
Name: "Nginx Datasource",
|
||||
Type: "datasource",
|
||||
Role: "Viewer",
|
||||
Slug: "nginx-datasource",
|
||||
},
|
||||
},
|
||||
},
|
||||
Class: plugins.External,
|
||||
Module: "plugins/test-app/module",
|
||||
BaseURL: "public/plugins/test-app",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/includes-symlinks"),
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "Load an unsigned plugin (development)",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
Env: "development",
|
||||
},
|
||||
pluginPaths: []string{"../testdata/unsigned-datasource"},
|
||||
want: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: "datasource",
|
||||
Name: "Test",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Grafana Labs",
|
||||
URL: "https://grafana.com",
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-datasource.svg",
|
||||
Large: "public/img/icn-datasource.svg",
|
||||
},
|
||||
Description: "Test",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
Backend: true,
|
||||
State: plugins.AlphaRelease,
|
||||
},
|
||||
Class: plugins.External,
|
||||
Module: "plugins/test/module",
|
||||
BaseURL: "public/plugins/test",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/unsigned-datasource/plugin"),
|
||||
Signature: "unsigned",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "Load an unsigned plugin (production)",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
Env: "production",
|
||||
},
|
||||
pluginPaths: []string{"../testdata/unsigned-datasource"},
|
||||
want: []*plugins.Plugin{},
|
||||
pluginErrors: map[string]*plugins.Error{
|
||||
"test": {
|
||||
PluginID: "test",
|
||||
ErrorCode: "signatureMissing",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Load an unsigned plugin using PluginsAllowUnsigned config (production)",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
Env: "production",
|
||||
PluginsAllowUnsigned: []string{"test"},
|
||||
},
|
||||
pluginPaths: []string{"../testdata/unsigned-datasource"},
|
||||
want: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: "datasource",
|
||||
Name: "Test",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Grafana Labs",
|
||||
URL: "https://grafana.com",
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-datasource.svg",
|
||||
Large: "public/img/icn-datasource.svg",
|
||||
},
|
||||
Description: "Test",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
Backend: true,
|
||||
State: plugins.AlphaRelease,
|
||||
},
|
||||
Class: plugins.External,
|
||||
Module: "plugins/test/module",
|
||||
BaseURL: "public/plugins/test",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/unsigned-datasource/plugin"),
|
||||
Signature: "unsigned",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Load an unsigned plugin with modified signature (production)",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
Env: "production",
|
||||
},
|
||||
pluginPaths: []string{"../testdata/lacking-files"},
|
||||
want: []*plugins.Plugin{},
|
||||
pluginErrors: map[string]*plugins.Error{
|
||||
"test": {
|
||||
PluginID: "test",
|
||||
ErrorCode: "signatureModified",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Load an unsigned plugin with modified signature using PluginsAllowUnsigned config (production) still includes a signing error",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
Env: "production",
|
||||
PluginsAllowUnsigned: []string{"test"},
|
||||
},
|
||||
pluginPaths: []string{"../testdata/lacking-files"},
|
||||
want: []*plugins.Plugin{},
|
||||
pluginErrors: map[string]*plugins.Error{
|
||||
"test": {
|
||||
PluginID: "test",
|
||||
ErrorCode: "signatureModified",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
l := newLoader(tt.cfg)
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := l.Load(tt.pluginPaths, tt.existingPlugins)
|
||||
require.NoError(t, err)
|
||||
if !cmp.Equal(got, tt.want, compareOpts) {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts))
|
||||
}
|
||||
|
||||
pluginErrs := l.PluginErrors()
|
||||
assert.Equal(t, len(tt.pluginErrors), len(pluginErrs))
|
||||
for _, pluginErr := range pluginErrs {
|
||||
assert.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoader_Load_MultiplePlugins(t *testing.T) {
|
||||
parentDir, err := filepath.Abs("../")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of current dir")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("Load multiple", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *setting.Cfg
|
||||
pluginPaths []string
|
||||
appURL string
|
||||
existingPlugins map[string]struct{}
|
||||
want []*plugins.Plugin
|
||||
pluginErrors map[string]*plugins.Error
|
||||
}{
|
||||
{
|
||||
name: "Load multiple plugins (broken, valid, unsigned)",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
},
|
||||
appURL: "http://localhost:3000",
|
||||
pluginPaths: []string{
|
||||
"../testdata/invalid-plugin-json", // test-app
|
||||
"../testdata/valid-v2-pvt-signature", // test
|
||||
"../testdata/unsigned-panel", // test-panel
|
||||
},
|
||||
want: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: "datasource",
|
||||
Name: "Test",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Will Browne",
|
||||
URL: "https://willbrowne.com",
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-datasource.svg",
|
||||
Large: "public/img/icn-datasource.svg",
|
||||
},
|
||||
Description: "Test",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
Backend: true,
|
||||
Executable: "test",
|
||||
State: plugins.AlphaRelease,
|
||||
},
|
||||
Class: plugins.External,
|
||||
Module: "plugins/test/module",
|
||||
BaseURL: "public/plugins/test",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/valid-v2-pvt-signature/plugin"),
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.PrivateSignature,
|
||||
SignatureOrg: "Will Browne",
|
||||
},
|
||||
},
|
||||
pluginErrors: map[string]*plugins.Error{
|
||||
"test": {
|
||||
PluginID: "test",
|
||||
ErrorCode: "signatureMissing",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
l := newLoader(tt.cfg)
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
origAppURL := setting.AppUrl
|
||||
t.Cleanup(func() {
|
||||
setting.AppUrl = origAppURL
|
||||
})
|
||||
setting.AppUrl = tt.appURL
|
||||
|
||||
got, err := l.Load(tt.pluginPaths, tt.existingPlugins)
|
||||
require.NoError(t, err)
|
||||
sort.SliceStable(got, func(i, j int) bool {
|
||||
return got[i].ID < got[j].ID
|
||||
})
|
||||
if !cmp.Equal(got, tt.want, compareOpts) {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoader_Signature_RootURL(t *testing.T) {
|
||||
const defaultAppURL = "http://localhost:3000/grafana"
|
||||
|
||||
parentDir, err := filepath.Abs("../")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of current dir")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("Private signature verification ignores trailing slash in root URL", func(t *testing.T) {
|
||||
origAppURL := setting.AppUrl
|
||||
origAppSubURL := setting.AppSubUrl
|
||||
t.Cleanup(func() {
|
||||
setting.AppUrl = origAppURL
|
||||
setting.AppSubUrl = origAppSubURL
|
||||
})
|
||||
setting.AppUrl = defaultAppURL
|
||||
|
||||
paths := []string{"../testdata/valid-v2-pvt-signature-root-url-uri"}
|
||||
|
||||
expected := []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: "datasource",
|
||||
Name: "Test",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{Name: "Will Browne", URL: "https://willbrowne.com"},
|
||||
Description: "Test",
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-datasource.svg",
|
||||
Large: "public/img/icn-datasource.svg",
|
||||
},
|
||||
Version: "1.0.0",
|
||||
},
|
||||
State: plugins.AlphaRelease,
|
||||
Dependencies: plugins.Dependencies{GrafanaVersion: "*", Plugins: []plugins.Dependency{}},
|
||||
Backend: true,
|
||||
Executable: "test",
|
||||
},
|
||||
PluginDir: filepath.Join(parentDir, "/testdata/valid-v2-pvt-signature-root-url-uri/plugin"),
|
||||
Class: "external",
|
||||
Signature: "valid",
|
||||
SignatureType: "private",
|
||||
SignatureOrg: "Will Browne",
|
||||
Module: "plugins/test/module",
|
||||
BaseURL: "public/plugins/test",
|
||||
},
|
||||
}
|
||||
|
||||
l := newLoader(&setting.Cfg{PluginsPath: filepath.Join(parentDir)})
|
||||
got, err := l.Load(paths, map[string]struct{}{})
|
||||
assert.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_DuplicatePlugins(t *testing.T) {
|
||||
t.Run("Load duplicate plugin folders", func(t *testing.T) {
|
||||
pluginDir, err := filepath.Abs("../testdata/test-app")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of plugin dir")
|
||||
return
|
||||
}
|
||||
expected := []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-app",
|
||||
Type: "app",
|
||||
Name: "Test App",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Test Inc.",
|
||||
URL: "http://test.com",
|
||||
},
|
||||
Description: "Official Grafana Test App & Dashboard bundle",
|
||||
Version: "1.0.0",
|
||||
Links: []plugins.InfoLink{
|
||||
{Name: "Project site", URL: "http://project.com"},
|
||||
{Name: "License & Terms", URL: "http://license.com"},
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/plugins/test-app/img/logo_small.png",
|
||||
Large: "public/plugins/test-app/img/logo_large.png",
|
||||
},
|
||||
Screenshots: []plugins.Screenshots{
|
||||
{Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"},
|
||||
{Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"},
|
||||
},
|
||||
Updated: "2015-02-10",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "3.x.x",
|
||||
Plugins: []plugins.Dependency{
|
||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: "Viewer", Slug: "nginx-connections"},
|
||||
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: "Viewer", Slug: "nginx-memory"},
|
||||
{Name: "Nginx Panel", Type: "panel", Role: "Viewer", Slug: "nginx-panel"},
|
||||
{Name: "Nginx Datasource", Type: "datasource", Role: "Viewer", Slug: "nginx-datasource"},
|
||||
},
|
||||
Backend: false,
|
||||
},
|
||||
PluginDir: pluginDir,
|
||||
Class: plugins.External,
|
||||
Signature: plugins.SignatureValid,
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
Module: "plugins/test-app/module",
|
||||
BaseURL: "public/plugins/test-app",
|
||||
},
|
||||
}
|
||||
|
||||
l := newLoader(&setting.Cfg{
|
||||
PluginsPath: filepath.Dir(pluginDir),
|
||||
})
|
||||
|
||||
got, err := l.Load([]string{pluginDir, pluginDir}, map[string]struct{}{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !cmp.Equal(got, expected, compareOpts) {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoader_loadNestedPlugins(t *testing.T) {
|
||||
parentDir, err := filepath.Abs("../")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of root dir")
|
||||
return
|
||||
}
|
||||
parent := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-ds",
|
||||
Type: "datasource",
|
||||
Name: "Parent",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Grafana Labs",
|
||||
URL: "http://grafana.com",
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-datasource.svg",
|
||||
Large: "public/img/icn-datasource.svg",
|
||||
},
|
||||
Description: "Parent plugin",
|
||||
Version: "1.0.0",
|
||||
Updated: "2020-10-20",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
Backend: true,
|
||||
},
|
||||
Module: "plugins/test-ds/module",
|
||||
BaseURL: "public/plugins/test-ds",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/nested-plugins/parent"),
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
Class: "external",
|
||||
}
|
||||
|
||||
child := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-panel",
|
||||
Type: "panel",
|
||||
Name: "Child",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Grafana Labs",
|
||||
URL: "http://grafana.com",
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-panel.svg",
|
||||
Large: "public/img/icn-panel.svg",
|
||||
},
|
||||
Description: "Child plugin",
|
||||
Version: "1.0.1",
|
||||
Updated: "2020-10-30",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
},
|
||||
Module: "plugins/test-panel/module",
|
||||
BaseURL: "public/plugins/test-panel",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/nested-plugins/parent/nested"),
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
Class: "external",
|
||||
}
|
||||
|
||||
parent.Children = []*plugins.Plugin{child}
|
||||
child.Parent = parent
|
||||
|
||||
t.Run("Load nested External plugins", func(t *testing.T) {
|
||||
expected := []*plugins.Plugin{parent, child}
|
||||
l := newLoader(&setting.Cfg{
|
||||
PluginsPath: parentDir,
|
||||
})
|
||||
|
||||
got, err := l.Load([]string{"../testdata/nested-plugins"}, map[string]struct{}{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// to ensure we can compare with expected
|
||||
sort.SliceStable(got, func(i, j int) bool {
|
||||
return got[i].ID < got[j].ID
|
||||
})
|
||||
|
||||
if !cmp.Equal(got, expected, compareOpts) {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Load will exclude plugins that already exist", func(t *testing.T) {
|
||||
// parent/child links will not be created when either plugins are provided in the existingPlugins map
|
||||
parent.Children = nil
|
||||
expected := []*plugins.Plugin{parent}
|
||||
|
||||
l := newLoader(&setting.Cfg{
|
||||
PluginsPath: parentDir,
|
||||
})
|
||||
|
||||
got, err := l.Load([]string{"../testdata/nested-plugins"}, map[string]struct{}{
|
||||
"test-panel": {},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// to ensure we can compare with expected
|
||||
sort.SliceStable(got, func(i, j int) bool {
|
||||
return got[i].ID < got[j].ID
|
||||
})
|
||||
|
||||
if !cmp.Equal(got, expected, compareOpts) {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoader_readPluginJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pluginPath string
|
||||
expected plugins.JSONData
|
||||
failed bool
|
||||
}{
|
||||
{
|
||||
name: "Valid plugin",
|
||||
pluginPath: "../testdata/test-app/plugin.json",
|
||||
expected: plugins.JSONData{
|
||||
ID: "test-app",
|
||||
Type: "app",
|
||||
Name: "Test App",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Test Inc.",
|
||||
URL: "http://test.com",
|
||||
},
|
||||
Description: "Official Grafana Test App & Dashboard bundle",
|
||||
Version: "1.0.0",
|
||||
Links: []plugins.InfoLink{
|
||||
{Name: "Project site", URL: "http://project.com"},
|
||||
{Name: "License & Terms", URL: "http://license.com"},
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "img/logo_small.png",
|
||||
Large: "img/logo_large.png",
|
||||
},
|
||||
Screenshots: []plugins.Screenshots{
|
||||
{Path: "img/screenshot1.png", Name: "img1"},
|
||||
{Path: "img/screenshot2.png", Name: "img2"},
|
||||
},
|
||||
Updated: "2015-02-10",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "3.x.x",
|
||||
Plugins: []plugins.Dependency{
|
||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard"},
|
||||
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard"},
|
||||
{Name: "Nginx Panel", Type: "panel"},
|
||||
{Name: "Nginx Datasource", Type: "datasource"},
|
||||
},
|
||||
Backend: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid plugin JSON",
|
||||
pluginPath: "../testdata/invalid-plugin-json/plugin.json",
|
||||
failed: true,
|
||||
},
|
||||
{
|
||||
name: "Non-existing JSON file",
|
||||
pluginPath: "nonExistingFile.json",
|
||||
failed: true,
|
||||
},
|
||||
}
|
||||
|
||||
l := newLoader(nil)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := l.readPluginJSON(tt.pluginPath)
|
||||
if (err != nil) && !tt.failed {
|
||||
t.Errorf("readPluginJSON() error = %v, failed %v", err, tt.failed)
|
||||
return
|
||||
}
|
||||
if !cmp.Equal(got, tt.expected, compareOpts) {
|
||||
t.Errorf("Unexpected pluginJSONData: %v", cmp.Diff(got, tt.expected, compareOpts))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_validatePluginJSON(t *testing.T) {
|
||||
type args struct {
|
||||
data plugins.JSONData
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "Valid case",
|
||||
args: args{
|
||||
data: plugins.JSONData{
|
||||
ID: "grafana-plugin-id",
|
||||
Type: plugins.DataSource,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid plugin ID",
|
||||
args: args{
|
||||
data: plugins.JSONData{
|
||||
Type: plugins.Panel,
|
||||
},
|
||||
},
|
||||
err: ErrInvalidPluginJSON,
|
||||
},
|
||||
{
|
||||
name: "Invalid plugin type",
|
||||
args: args{
|
||||
data: plugins.JSONData{
|
||||
ID: "grafana-plugin-id",
|
||||
Type: "test",
|
||||
},
|
||||
},
|
||||
err: ErrInvalidPluginJSON,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := validatePluginJSON(tt.args.data); !errors.Is(err, tt.err) {
|
||||
t.Errorf("validatePluginJSON() = %v, want %v", err, tt.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_pluginClass(t *testing.T) {
|
||||
type args struct {
|
||||
pluginDir string
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected plugins.Class
|
||||
}{
|
||||
{
|
||||
name: "Core plugin class",
|
||||
args: args{
|
||||
pluginDir: "/root/app/plugins/test-app",
|
||||
cfg: &setting.Cfg{
|
||||
StaticRootPath: "/root",
|
||||
},
|
||||
},
|
||||
expected: plugins.Core,
|
||||
},
|
||||
{
|
||||
name: "Bundled plugin class",
|
||||
args: args{
|
||||
pluginDir: "/test-app",
|
||||
cfg: &setting.Cfg{
|
||||
BundledPluginsPath: "/test-app",
|
||||
},
|
||||
},
|
||||
expected: plugins.Bundled,
|
||||
},
|
||||
{
|
||||
name: "External plugin class",
|
||||
args: args{
|
||||
pluginDir: "/test-app",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: "/test-app",
|
||||
},
|
||||
},
|
||||
expected: plugins.External,
|
||||
},
|
||||
{
|
||||
name: "External plugin class",
|
||||
args: args{
|
||||
pluginDir: "/test-app",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: "/root",
|
||||
},
|
||||
},
|
||||
expected: plugins.External,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := newLoader(tt.args.cfg)
|
||||
got := l.pluginClass(tt.args.pluginDir)
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newLoader(cfg *setting.Cfg) *Loader {
|
||||
return &Loader{
|
||||
cfg: cfg,
|
||||
pluginFinder: finder.New(cfg),
|
||||
pluginInitializer: initializer.New(cfg, &fakeLicensingService{}),
|
||||
signatureValidator: signature.NewValidator(cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}),
|
||||
errs: make(map[string]*plugins.SignatureError),
|
||||
}
|
||||
}
|
||||
|
||||
type fakeLicensingService struct {
|
||||
edition string
|
||||
hasLicense bool
|
||||
tokenRaw string
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) HasLicense() bool {
|
||||
return t.hasLicense
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) Expiry() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) Edition() string {
|
||||
return t.edition
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) StateInfo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) ContentDeliveryPrefix() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) LicenseURL(showAdminLicensingPage bool) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) HasValidLicense() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) Environment() map[string]string {
|
||||
return map[string]string{"GF_ENTERPRISE_LICENSE_TEXT": t.tokenRaw}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ type InfraLogWrapper struct {
|
||||
debugMode bool
|
||||
}
|
||||
|
||||
func NewInstallerLogger(name string, debugMode bool) (l *InfraLogWrapper) {
|
||||
func newInstallerLogger(name string, debugMode bool) (l *InfraLogWrapper) {
|
||||
return &InfraLogWrapper{
|
||||
debugMode: debugMode,
|
||||
l: log.New(name),
|
||||
|
||||
+623
-630
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,165 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
func TestPluginManager_int_init(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
staticRootPath, err := filepath.Abs("../../../public/")
|
||||
require.NoError(t, err)
|
||||
|
||||
bundledPluginsPath, err := filepath.Abs("../../../plugins-bundled/internal")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &setting.Cfg{
|
||||
Raw: ini.Empty(),
|
||||
Env: setting.Prod,
|
||||
StaticRootPath: staticRootPath,
|
||||
BundledPluginsPath: bundledPluginsPath,
|
||||
PluginSettings: map[string]map[string]string{
|
||||
"plugin.datasource-id": {
|
||||
"path": "testdata/test-app",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
license := &licensing.OSSLicensingService{
|
||||
Cfg: cfg,
|
||||
}
|
||||
pm := newManager(cfg, nil, loader.New(license, cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}), nil)
|
||||
|
||||
err = pm.init()
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyCorePluginCatalogue(t, pm)
|
||||
verifyBundledPlugins(t, pm)
|
||||
verifyPluginStaticRoutes(t, pm)
|
||||
}
|
||||
|
||||
func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) {
|
||||
t.Helper()
|
||||
|
||||
expPanels := map[string]struct{}{
|
||||
"alertGroups": {},
|
||||
"alertlist": {},
|
||||
"annolist": {},
|
||||
"barchart": {},
|
||||
"bargauge": {},
|
||||
"canvas": {},
|
||||
"dashlist": {},
|
||||
"debug": {},
|
||||
"gauge": {},
|
||||
"geomap": {},
|
||||
"gettingstarted": {},
|
||||
"graph": {},
|
||||
"heatmap": {},
|
||||
"histogram": {},
|
||||
"icon": {},
|
||||
"live": {},
|
||||
"logs": {},
|
||||
"news": {},
|
||||
"nodeGraph": {},
|
||||
"piechart": {},
|
||||
"pluginlist": {},
|
||||
"stat": {},
|
||||
"state-timeline": {},
|
||||
"status-history": {},
|
||||
"table": {},
|
||||
"table-old": {},
|
||||
"text": {},
|
||||
"timeseries": {},
|
||||
"welcome": {},
|
||||
"xychart": {},
|
||||
}
|
||||
|
||||
expDataSources := map[string]struct{}{
|
||||
"alertmanager": {},
|
||||
"dashboard": {},
|
||||
"input": {},
|
||||
"jaeger": {},
|
||||
"mixed": {},
|
||||
"zipkin": {},
|
||||
}
|
||||
|
||||
expApps := map[string]struct{}{
|
||||
"test-app": {},
|
||||
}
|
||||
|
||||
panels := pm.Plugins(plugins.Panel)
|
||||
assert.Equal(t, len(expPanels), len(panels))
|
||||
for _, p := range panels {
|
||||
require.NotNil(t, pm.Plugin(p.ID))
|
||||
assert.Contains(t, expPanels, p.ID)
|
||||
assert.Contains(t, pm.registeredPlugins(), p.ID)
|
||||
}
|
||||
|
||||
dataSources := pm.Plugins(plugins.DataSource)
|
||||
assert.Equal(t, len(expDataSources), len(dataSources))
|
||||
for _, ds := range dataSources {
|
||||
require.NotNil(t, pm.Plugin(ds.ID))
|
||||
assert.Contains(t, expDataSources, ds.ID)
|
||||
assert.Contains(t, pm.registeredPlugins(), ds.ID)
|
||||
}
|
||||
|
||||
apps := pm.Plugins(plugins.App)
|
||||
assert.Equal(t, len(expApps), len(apps))
|
||||
for _, app := range apps {
|
||||
require.NotNil(t, pm.Plugin(app.ID))
|
||||
require.Contains(t, expApps, app.ID)
|
||||
assert.Contains(t, pm.registeredPlugins(), app.ID)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(expPanels)+len(expDataSources)+len(expApps), len(pm.Plugins()))
|
||||
}
|
||||
|
||||
func verifyBundledPlugins(t *testing.T, pm *PluginManager) {
|
||||
t.Helper()
|
||||
|
||||
dsPlugins := make(map[string]struct{})
|
||||
for _, p := range pm.Plugins(plugins.DataSource) {
|
||||
dsPlugins[p.ID] = struct{}{}
|
||||
}
|
||||
|
||||
pluginRoutes := make(map[string]*plugins.StaticRoute)
|
||||
for _, r := range pm.Routes() {
|
||||
pluginRoutes[r.PluginID] = r
|
||||
}
|
||||
|
||||
assert.NotNil(t, pm.Plugin("input"))
|
||||
assert.NotNil(t, dsPlugins["input"])
|
||||
|
||||
for _, pluginID := range []string{"input"} {
|
||||
assert.Contains(t, pluginRoutes, pluginID)
|
||||
assert.True(t, strings.HasPrefix(pluginRoutes[pluginID].Directory, pm.Plugin("input").PluginDir))
|
||||
}
|
||||
}
|
||||
|
||||
func verifyPluginStaticRoutes(t *testing.T, pm *PluginManager) {
|
||||
pluginRoutes := make(map[string]*plugins.StaticRoute)
|
||||
for _, route := range pm.Routes() {
|
||||
pluginRoutes[route.PluginID] = route
|
||||
}
|
||||
|
||||
assert.Len(t, pluginRoutes, 2)
|
||||
|
||||
assert.Contains(t, pluginRoutes, "input")
|
||||
assert.Equal(t, pluginRoutes["input"].Directory, pm.Plugin("input").PluginDir)
|
||||
|
||||
assert.Contains(t, pluginRoutes, "test-app")
|
||||
assert.Equal(t, pluginRoutes["test-app"].Directory, pm.Plugin("test-app").PluginDir)
|
||||
}
|
||||
+666
-650
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
func (pm *PluginManager) GetPluginSettings(orgID int64) (map[string]*models.PluginSettingInfoDTO, error) {
|
||||
pluginSettings, err := pm.SQLStore.GetPluginSettings(orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginMap := make(map[string]*models.PluginSettingInfoDTO)
|
||||
for _, plug := range pluginSettings {
|
||||
pluginMap[plug.PluginId] = plug
|
||||
}
|
||||
|
||||
for _, pluginDef := range pm.Plugins() {
|
||||
// ignore entries that exists
|
||||
if _, ok := pluginMap[pluginDef.Id]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// default to enabled true
|
||||
opt := &models.PluginSettingInfoDTO{
|
||||
PluginId: pluginDef.Id,
|
||||
OrgId: orgID,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
// apps are disabled by default unless autoEnabled: true
|
||||
if app, exists := pm.apps[pluginDef.Id]; exists {
|
||||
opt.Enabled = app.AutoEnabled
|
||||
opt.Pinned = app.AutoEnabled
|
||||
}
|
||||
|
||||
// if it's included in app check app settings
|
||||
if pluginDef.IncludedInAppId != "" {
|
||||
// app components are by default disabled
|
||||
opt.Enabled = false
|
||||
|
||||
if appSettings, ok := pluginMap[pluginDef.IncludedInAppId]; ok {
|
||||
opt.Enabled = appSettings.Enabled
|
||||
}
|
||||
}
|
||||
|
||||
pluginMap[pluginDef.Id] = opt
|
||||
}
|
||||
|
||||
return pluginMap, nil
|
||||
}
|
||||
|
||||
func (pm *PluginManager) GetEnabledPlugins(orgID int64) (*plugins.EnabledPlugins, error) {
|
||||
enabledPlugins := &plugins.EnabledPlugins{
|
||||
Panels: make([]*plugins.PanelPlugin, 0),
|
||||
DataSources: make(map[string]*plugins.DataSourcePlugin),
|
||||
Apps: make([]*plugins.AppPlugin, 0),
|
||||
}
|
||||
|
||||
pluginSettingMap, err := pm.GetPluginSettings(orgID)
|
||||
if err != nil {
|
||||
return enabledPlugins, err
|
||||
}
|
||||
|
||||
for _, app := range pm.Apps() {
|
||||
if b, ok := pluginSettingMap[app.Id]; ok {
|
||||
app.Pinned = b.Pinned
|
||||
enabledPlugins.Apps = append(enabledPlugins.Apps, app)
|
||||
}
|
||||
}
|
||||
|
||||
// add all plugins that are not part of an App.
|
||||
for dsID, ds := range pm.dataSources {
|
||||
if _, exists := pluginSettingMap[ds.Id]; exists {
|
||||
enabledPlugins.DataSources[dsID] = ds
|
||||
}
|
||||
}
|
||||
|
||||
for _, panel := range pm.panels {
|
||||
if _, exists := pluginSettingMap[panel.Id]; exists {
|
||||
enabledPlugins.Panels = append(enabledPlugins.Panels, panel)
|
||||
}
|
||||
}
|
||||
|
||||
return enabledPlugins, nil
|
||||
}
|
||||
|
||||
// IsAppInstalled checks if an app plugin with provided plugin ID is installed.
|
||||
func (pm *PluginManager) IsAppInstalled(pluginID string) bool {
|
||||
_, exists := pm.apps[pluginID]
|
||||
return exists
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func ProvideService(cfg *setting.Cfg) (*UnsignedPluginAuthorizer, error) {
|
||||
return &UnsignedPluginAuthorizer{
|
||||
Cfg: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type UnsignedPluginAuthorizer struct {
|
||||
Cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func (u *UnsignedPluginAuthorizer) CanLoadPlugin(p *plugins.Plugin) bool {
|
||||
if p.Signature != plugins.SignatureUnsigned {
|
||||
return true
|
||||
}
|
||||
|
||||
if u.Cfg.Env == setting.Dev {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, pID := range u.Cfg.PluginsAllowUnsigned {
|
||||
if pID == p.ID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package manager
|
||||
package signature
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -15,13 +15,13 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/openpgp"
|
||||
"golang.org/x/crypto/openpgp/clearsign"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
|
||||
"golang.org/x/crypto/openpgp"
|
||||
"golang.org/x/crypto/openpgp/clearsign"
|
||||
)
|
||||
|
||||
// Soon we can fetch keys from:
|
||||
@@ -59,11 +59,11 @@ type pluginManifest struct {
|
||||
Files map[string]string `json:"files"`
|
||||
|
||||
// V2 supported fields
|
||||
ManifestVersion string `json:"manifestVersion"`
|
||||
SignatureType plugins.PluginSignatureType `json:"signatureType"`
|
||||
SignedByOrg string `json:"signedByOrg"`
|
||||
SignedByOrgName string `json:"signedByOrgName"`
|
||||
RootURLs []string `json:"rootUrls"`
|
||||
ManifestVersion string `json:"manifestVersion"`
|
||||
SignatureType plugins.SignatureType `json:"signatureType"`
|
||||
SignedByOrg string `json:"signedByOrg"`
|
||||
SignedByOrgName string `json:"signedByOrgName"`
|
||||
RootURLs []string `json:"rootUrls"`
|
||||
}
|
||||
|
||||
func (m *pluginManifest) isV2() bool {
|
||||
@@ -99,9 +99,13 @@ func readPluginManifest(body []byte) (*pluginManifest, error) {
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// getPluginSignatureState returns the signature state for a plugin.
|
||||
func getPluginSignatureState(log log.Logger, plugin *plugins.PluginBase) (plugins.PluginSignatureState, error) {
|
||||
log.Debug("Getting signature state of plugin", "plugin", plugin.Id, "isBackend", plugin.Backend)
|
||||
func Calculate(log log.Logger, plugin *plugins.Plugin) (plugins.Signature, error) {
|
||||
if plugin.IsCorePlugin() {
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInternal,
|
||||
}, nil
|
||||
}
|
||||
|
||||
manifestPath := filepath.Join(plugin.PluginDir, "MANIFEST.txt")
|
||||
|
||||
// nolint:gosec
|
||||
@@ -109,62 +113,55 @@ func getPluginSignatureState(log log.Logger, plugin *plugins.PluginBase) (plugin
|
||||
// on plugin the folder structure on disk and not user input.
|
||||
byteValue, err := ioutil.ReadFile(manifestPath)
|
||||
if err != nil || len(byteValue) < 10 {
|
||||
log.Debug("Plugin is unsigned", "id", plugin.Id)
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureUnsigned,
|
||||
log.Debug("Plugin is unsigned", "id", plugin.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureUnsigned,
|
||||
}, nil
|
||||
}
|
||||
|
||||
manifest, err := readPluginManifest(byteValue)
|
||||
if err != nil {
|
||||
log.Debug("Plugin signature invalid", "id", plugin.Id)
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureInvalid,
|
||||
log.Debug("Plugin signature invalid", "id", plugin.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInvalid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Make sure the versions all match
|
||||
if manifest.Plugin != plugin.Id || manifest.Version != plugin.Info.Version {
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureModified,
|
||||
if manifest.Plugin != plugin.ID || manifest.Version != plugin.Info.Version {
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureModified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate that private is running within defined root URLs
|
||||
if manifest.SignatureType == plugins.PrivateType {
|
||||
if manifest.SignatureType == plugins.PrivateSignature {
|
||||
appURL, err := url.Parse(setting.AppUrl)
|
||||
if err != nil {
|
||||
return plugins.PluginSignatureState{}, err
|
||||
return plugins.Signature{}, err
|
||||
}
|
||||
appSubURL, err := url.Parse(setting.AppSubUrl)
|
||||
if err != nil {
|
||||
return plugins.PluginSignatureState{}, err
|
||||
}
|
||||
appURLPath := path.Join(appSubURL.RequestURI(), appURL.RequestURI())
|
||||
|
||||
foundMatch := false
|
||||
for _, u := range manifest.RootURLs {
|
||||
rootURL, err := url.Parse(u)
|
||||
if err != nil {
|
||||
log.Warn("Could not parse plugin root URL", "plugin", plugin.Id, "rootUrl", rootURL)
|
||||
return plugins.PluginSignatureState{}, err
|
||||
log.Warn("Could not parse plugin root URL", "plugin", plugin.ID, "rootUrl", rootURL)
|
||||
return plugins.Signature{}, err
|
||||
}
|
||||
|
||||
if rootURL.Scheme == appURL.Scheme &&
|
||||
rootURL.Host == appURL.Host {
|
||||
foundMatch = path.Clean(rootURL.RequestURI()) == appURLPath
|
||||
|
||||
if foundMatch {
|
||||
break
|
||||
}
|
||||
rootURL.Host == appURL.Host &&
|
||||
path.Clean(rootURL.RequestURI()) == path.Clean(appURL.RequestURI()) {
|
||||
foundMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundMatch {
|
||||
log.Warn("Could not find root URL that matches running application URL", "plugin", plugin.Id,
|
||||
log.Warn("Could not find root URL that matches running application URL", "plugin", plugin.ID,
|
||||
"appUrl", appURL, "rootUrls", manifest.RootURLs)
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureInvalid,
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInvalid,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -172,24 +169,23 @@ func getPluginSignatureState(log log.Logger, plugin *plugins.PluginBase) (plugin
|
||||
manifestFiles := make(map[string]struct{}, len(manifest.Files))
|
||||
|
||||
// Verify the manifest contents
|
||||
log.Debug("Verifying contents of plugin manifest", "plugin", plugin.Id)
|
||||
for fp, hash := range manifest.Files {
|
||||
err = verifyHash(plugin.Id, filepath.Join(plugin.PluginDir, fp), hash)
|
||||
for p, hash := range manifest.Files {
|
||||
err = verifyHash(plugin.ID, filepath.Join(plugin.PluginDir, p), hash)
|
||||
if err != nil {
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureModified,
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureModified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
manifestFiles[fp] = struct{}{}
|
||||
manifestFiles[p] = struct{}{}
|
||||
}
|
||||
|
||||
if manifest.isV2() {
|
||||
pluginFiles, err := pluginFilesRequiringVerification(plugin)
|
||||
if err != nil {
|
||||
log.Warn("Could not collect plugin file information in directory", "pluginID", plugin.Id, "dir", plugin.PluginDir)
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureInvalid,
|
||||
log.Warn("Could not collect plugin file information in directory", "pluginID", plugin.ID, "dir", plugin.PluginDir)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInvalid,
|
||||
}, err
|
||||
}
|
||||
|
||||
@@ -202,20 +198,18 @@ func getPluginSignatureState(log log.Logger, plugin *plugins.PluginBase) (plugin
|
||||
}
|
||||
|
||||
if len(unsignedFiles) > 0 {
|
||||
log.Warn("The following files were not included in the signature", "plugin", plugin.Id, "files", unsignedFiles)
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureModified,
|
||||
log.Warn("The following files were not included in the signature", "plugin", plugin.ID, "files", unsignedFiles)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureModified,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Everything OK
|
||||
log.Debug("Plugin signature valid", "id", plugin.Id)
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureValid,
|
||||
log.Debug("Plugin signature valid", "id", plugin.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureValid,
|
||||
Type: manifest.SignatureType,
|
||||
SigningOrg: manifest.SignedByOrgName,
|
||||
Files: manifestFiles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -247,9 +241,9 @@ func verifyHash(pluginID string, path string, hash string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// gets plugin filenames that require verification for plugin signing
|
||||
// pluginFilesRequiringVerification gets plugin filenames that require verification for plugin signing
|
||||
// returns filenames as a slice of posix style paths relative to plugin directory
|
||||
func pluginFilesRequiringVerification(plugin *plugins.PluginBase) ([]string, error) {
|
||||
func pluginFilesRequiringVerification(plugin *plugins.Plugin) ([]string, error) {
|
||||
var files []string
|
||||
err := filepath.Walk(plugin.PluginDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package manager
|
||||
package signature
|
||||
|
||||
import (
|
||||
"sort"
|
||||
@@ -105,7 +105,7 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
|
||||
assert.Equal(t, int64(1605807018050), manifest.Time)
|
||||
assert.Equal(t, "7e4d0c6a708866e7", manifest.KeyID)
|
||||
assert.Equal(t, "2.0.0", manifest.ManifestVersion)
|
||||
assert.Equal(t, plugins.PrivateType, manifest.SignatureType)
|
||||
assert.Equal(t, plugins.PrivateSignature, manifest.SignatureType)
|
||||
assert.Equal(t, "willbrowne", manifest.SignedByOrg)
|
||||
assert.Equal(t, "Will Browne", manifest.SignedByOrgName)
|
||||
assert.Equal(t, []string{"http://localhost:3000/"}, manifest.RootURLs)
|
||||
@@ -0,0 +1,81 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var logger = log.New("plugin.signature.validator")
|
||||
|
||||
type Validator struct {
|
||||
cfg *setting.Cfg
|
||||
authorizer plugins.PluginLoaderAuthorizer
|
||||
}
|
||||
|
||||
func NewValidator(cfg *setting.Cfg, authorizer plugins.PluginLoaderAuthorizer) Validator {
|
||||
return Validator{
|
||||
cfg: cfg,
|
||||
authorizer: authorizer,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Validator) Validate(plugin *plugins.Plugin) *plugins.SignatureError {
|
||||
if plugin.Signature == plugins.SignatureValid {
|
||||
logger.Debug("Plugin has valid signature", "id", plugin.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If a plugin is nested within another, create links to each other to inherit signature details
|
||||
if plugin.Parent != nil {
|
||||
if plugin.IsCorePlugin() || plugin.Signature == plugins.SignatureInternal {
|
||||
logger.Debug("Not setting descendant plugin's signature to that of root since it's core or internal",
|
||||
"plugin", plugin.ID, "signature", plugin.Signature, "isCore", plugin.IsCorePlugin)
|
||||
} else {
|
||||
logger.Debug("Setting descendant plugin's signature to that of root", "plugin", plugin.ID,
|
||||
"root", plugin.Parent.ID, "signature", plugin.Signature, "rootSignature", plugin.Parent.Signature)
|
||||
plugin.Signature = plugin.Parent.Signature
|
||||
plugin.SignatureType = plugin.Parent.SignatureType
|
||||
plugin.SignatureOrg = plugin.Parent.SignatureOrg
|
||||
if plugin.Signature == plugins.SignatureValid {
|
||||
logger.Debug("Plugin has valid signature (inherited from root)", "id", plugin.ID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if plugin.IsCorePlugin() || plugin.IsBundledPlugin() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch plugin.Signature {
|
||||
case plugins.SignatureUnsigned:
|
||||
if authorized := s.authorizer.CanLoadPlugin(plugin); !authorized {
|
||||
logger.Debug("Plugin is unsigned", "pluginID", plugin.ID)
|
||||
return &plugins.SignatureError{
|
||||
PluginID: plugin.ID,
|
||||
SignatureStatus: plugins.SignatureUnsigned,
|
||||
}
|
||||
}
|
||||
logger.Warn("Permitting unsigned plugin. This is not recommended", "pluginID", plugin.ID, "pluginDir", plugin.PluginDir)
|
||||
return nil
|
||||
case plugins.SignatureInvalid:
|
||||
logger.Debug("Plugin has an invalid signature", "pluginID", plugin.ID)
|
||||
return &plugins.SignatureError{
|
||||
PluginID: plugin.ID,
|
||||
SignatureStatus: plugins.SignatureInvalid,
|
||||
}
|
||||
case plugins.SignatureModified:
|
||||
logger.Debug("Plugin has a modified signature", "pluginID", plugin.ID)
|
||||
return &plugins.SignatureError{
|
||||
PluginID: plugin.ID,
|
||||
SignatureStatus: plugins.SignatureModified,
|
||||
}
|
||||
default:
|
||||
logger.Debug("Plugin has an unrecognized plugin signature state", "pluginID", plugin.ID, "signature",
|
||||
plugin.Signature)
|
||||
return &plugins.SignatureError{
|
||||
PluginID: plugin.ID,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"id": "test-app",
|
||||
"type": "application"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
{
|
||||
"manifestVersion": "2.0.0",
|
||||
"signatureType": "grafana",
|
||||
"signedByOrg": "grafana",
|
||||
"signedByOrgName": "Grafana Labs",
|
||||
"plugin": "test-ds",
|
||||
"version": "1.0.0",
|
||||
"time": 1629461930434,
|
||||
"keyId": "7e4d0c6a708866e7",
|
||||
"files": {
|
||||
"plugin.json": "64e98031f30cfada473e0ad4b989ac10cd0c86844aab8c0d3fc36d8a9537a0b8",
|
||||
"nested/plugin.json": "e64abd35cd211e0e4682974ad5cdd1be7a0b7cd24951d302a16d9e2cb6cefea4"
|
||||
}
|
||||
}
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: OpenPGP.js v4.10.1
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
wqIEARMKAAYFAmEfnaoACgkQfk0ManCIZufwYgIJAZULZ72BKYehVw362aOJ
|
||||
IkUhCaIceQT6rSmWw60Ksxs8xkeCebMPfuxm6xqpvoquVmD2zIirCFUXE41M
|
||||
SQBys7/aAgkBaaVZvVPLUMYHIGNQXQ0wJ0j6JGn5Mn25GH4lH4vttaCFpQmx
|
||||
zwV8J/s7Ho612fU1ijH/nFM97I4nfxonQUEyEbA=
|
||||
=7sr3
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Child",
|
||||
"id": "test-panel",
|
||||
"info": {
|
||||
"description": "Child plugin",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "http://grafana.com"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"updated": "2020-10-30"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "Parent",
|
||||
"id": "test-ds",
|
||||
"backend": true,
|
||||
"info": {
|
||||
"description": "Parent plugin",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "http://grafana.com"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"updated": "2020-10-20"
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
@@ -16,38 +15,20 @@ var (
|
||||
httpClient = http.Client{Timeout: 10 * time.Second}
|
||||
)
|
||||
|
||||
type grafanaNetPlugin struct {
|
||||
type gcomPlugin struct {
|
||||
Slug string `json:"slug"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type gitHubLatest struct {
|
||||
Stable string `json:"stable"`
|
||||
Testing string `json:"testing"`
|
||||
}
|
||||
|
||||
func (pm *PluginManager) getAllExternalPluginSlugs() string {
|
||||
var result []string
|
||||
for _, plug := range pm.plugins {
|
||||
if plug.IsCorePlugin {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, plug.Id)
|
||||
}
|
||||
|
||||
return strings.Join(result, ",")
|
||||
}
|
||||
|
||||
func (pm *PluginManager) checkForUpdates() {
|
||||
if !pm.Cfg.CheckForUpdates {
|
||||
func (m *PluginManager) checkForUpdates() {
|
||||
if !m.cfg.CheckForUpdates {
|
||||
return
|
||||
}
|
||||
|
||||
pm.log.Debug("Checking for updates")
|
||||
m.log.Debug("Checking for updates")
|
||||
|
||||
pluginSlugs := pm.getAllExternalPluginSlugs()
|
||||
resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
|
||||
pluginSlugs := m.externalPluginIDsAsCSV()
|
||||
resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + m.cfg.BuildVersion)
|
||||
if err != nil {
|
||||
log.Debug("Failed to get plugins repo from grafana.com", "error", err.Error())
|
||||
return
|
||||
@@ -64,64 +45,40 @@ func (pm *PluginManager) checkForUpdates() {
|
||||
return
|
||||
}
|
||||
|
||||
gNetPlugins := []grafanaNetPlugin{}
|
||||
err = json.Unmarshal(body, &gNetPlugins)
|
||||
var gcomPlugins []gcomPlugin
|
||||
err = json.Unmarshal(body, &gcomPlugins)
|
||||
if err != nil {
|
||||
log.Debug("Failed to unmarshal plugin repo, reading response from grafana.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, plug := range pm.Plugins() {
|
||||
for _, gplug := range gNetPlugins {
|
||||
if gplug.Slug == plug.Id {
|
||||
plug.GrafanaNetVersion = gplug.Version
|
||||
for _, localP := range m.Plugins() {
|
||||
for _, gcomP := range gcomPlugins {
|
||||
if gcomP.Slug == localP.ID {
|
||||
localP.GrafanaComVersion = gcomP.Version
|
||||
|
||||
plugVersion, err1 := version.NewVersion(plug.Info.Version)
|
||||
gplugVersion, err2 := version.NewVersion(gplug.Version)
|
||||
plugVersion, err1 := version.NewVersion(localP.Info.Version)
|
||||
gplugVersion, err2 := version.NewVersion(gcomP.Version)
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion
|
||||
localP.GrafanaComHasUpdate = localP.Info.Version != localP.GrafanaComVersion
|
||||
} else {
|
||||
plug.GrafanaNetHasUpdate = plugVersion.LessThan(gplugVersion)
|
||||
localP.GrafanaComHasUpdate = plugVersion.LessThan(gplugVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp2, err := httpClient.Get("https://raw.githubusercontent.com/grafana/grafana/main/latest.json")
|
||||
if err != nil {
|
||||
log.Debug("Failed to get latest.json repo from github.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := resp2.Body.Close(); err != nil {
|
||||
pm.log.Warn("Failed to close response body", "err", err)
|
||||
}
|
||||
}()
|
||||
body, err = ioutil.ReadAll(resp2.Body)
|
||||
if err != nil {
|
||||
log.Debug("Update check failed, reading response from github.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var latest gitHubLatest
|
||||
err = json.Unmarshal(body, &latest)
|
||||
if err != nil {
|
||||
log.Debug("Failed to unmarshal github.com latest, reading response from github.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(setting.BuildVersion, "-") {
|
||||
pm.grafanaLatestVersion = latest.Testing
|
||||
pm.grafanaHasUpdate = !strings.HasPrefix(setting.BuildVersion, latest.Testing)
|
||||
} else {
|
||||
pm.grafanaLatestVersion = latest.Stable
|
||||
pm.grafanaHasUpdate = latest.Stable != setting.BuildVersion
|
||||
}
|
||||
|
||||
currVersion, err1 := version.NewVersion(setting.BuildVersion)
|
||||
latestVersion, err2 := version.NewVersion(pm.grafanaLatestVersion)
|
||||
if err1 == nil && err2 == nil {
|
||||
pm.grafanaHasUpdate = currVersion.LessThan(latestVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *PluginManager) externalPluginIDsAsCSV() string {
|
||||
var result []string
|
||||
for _, p := range m.plugins {
|
||||
if p.IsCorePlugin() {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, p.ID)
|
||||
}
|
||||
|
||||
return strings.Join(result, ",")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user