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:
Will Browne
2021-11-01 10:53:33 +01:00
committed by GitHub
parent f4282571c7
commit b80fbe03f0
136 changed files with 7140 additions and 4486 deletions
-61
View File
@@ -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
+4 -2
View File
@@ -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)
+70 -10
View File
@@ -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 -3
View File
@@ -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)
-11
View File
@@ -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"
)
+15
View File
@@ -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{})
}
+3 -3
View File
@@ -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
}
+297
View File
@@ -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{}{}
}
}
+928
View File
@@ -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}
}
+1 -1
View File
@@ -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),
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)
}
File diff suppressed because it is too large Load Diff
-93
View File
@@ -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 {
@@ -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"
}
}
+29 -72
View File
@@ -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, ",")
}