Introduce TSDB service (#31520)

* Introduce TSDB service

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

Co-authored-by: Erik Sundell <erik.sundell87@gmail.com>
Co-authored-by: Will Browne <will.browne@grafana.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
Arve Knudsen
2021-03-08 07:02:49 +01:00
committed by GitHub
co-authored by Erik Sundell Will Browne Torkel Ödegaard Will Browne Zoltán Bedi
parent c899bf3592
commit b79e61656a
203 changed files with 5269 additions and 4776 deletions
+170
View File
@@ -0,0 +1,170 @@
package manager
import (
"encoding/json"
"fmt"
"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"
"github.com/grafana/grafana/pkg/tsdb/tsdbifaces"
)
var varRegex = regexp.MustCompile(`(\$\{.+?\})`)
type ImportDashboardInput struct {
Type string `json:"type"`
PluginId string `json:"pluginId"`
Name string `json:"name"`
Value string `json:"value"`
}
type DashboardInputMissingError struct {
VariableName string
}
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 []ImportDashboardInput, user *models.SignedInUser,
requestHandler tsdbifaces.RequestHandler) (plugins.PluginDashboardInfoDTO, error) {
var dashboard *models.Dashboard
if pluginID != "" {
var err error
if dashboard, err = pm.LoadPluginDashboard(pluginID, path); err != nil {
return plugins.PluginDashboardInfoDTO{}, err
}
} else {
dashboard = models.NewDashboardFromJson(dashboardModel)
}
evaluator := &DashTemplateEvaluator{
template: dashboard.Data,
inputs: inputs,
}
generatedDash, err := evaluator.Eval()
if err != nil {
return plugins.PluginDashboardInfoDTO{}, 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(requestHandler).ImportDashboard(dto)
if err != nil {
return plugins.PluginDashboardInfoDTO{}, 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,
}, nil
}
type DashTemplateEvaluator struct {
template *simplejson.Json
inputs []ImportDashboardInput
variables map[string]string
result *simplejson.Json
}
func (e *DashTemplateEvaluator) findInput(varName string, varType string) *ImportDashboardInput {
for _, input := range e.inputs {
if varType == input.Type && (input.Name == varName || input.Name == "*") {
return &input
}
}
return nil
}
func (e *DashTemplateEvaluator) Eval() (*simplejson.Json, error) {
e.result = simplejson.New()
e.variables = make(map[string]string)
// check that we have all inputs we need
for _, inputDef := range e.template.Get("__inputs").MustArray() {
inputDefJson := simplejson.NewFromAny(inputDef)
inputName := inputDefJson.Get("name").MustString()
inputType := inputDefJson.Get("type").MustString()
input := e.findInput(inputName, inputType)
if input == nil {
return nil, &DashboardInputMissingError{VariableName: inputName}
}
e.variables["${"+inputName+"}"] = input.Value
}
return simplejson.NewFromAny(e.evalObject(e.template)), nil
}
func (e *DashTemplateEvaluator) evalValue(source *simplejson.Json) interface{} {
sourceValue := source.Interface()
switch v := sourceValue.(type) {
case string:
interpolated := varRegex.ReplaceAllStringFunc(v, func(match string) string {
replacement, exists := e.variables[match]
if exists {
return replacement
}
return match
})
return interpolated
case bool:
return v
case json.Number:
return v
case map[string]interface{}:
return e.evalObject(source)
case []interface{}:
array := make([]interface{}, 0)
for _, item := range v {
array = append(array, e.evalValue(simplejson.NewFromAny(item)))
}
return array
}
return nil
}
func (e *DashTemplateEvaluator) evalObject(source *simplejson.Json) interface{} {
result := make(map[string]interface{})
for key, value := range source.MustMap() {
if key == "__inputs" {
continue
}
result[key] = e.evalValue(simplejson.NewFromAny(value))
}
return result
}
@@ -0,0 +1,97 @@
package manager
import (
"io/ioutil"
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func TestDashboardImport(t *testing.T) {
pluginScenario(t, "When importing a plugin dashboard", func(t *testing.T, pm *PluginManager) {
origNewDashboardService := dashboards.NewService
t.Cleanup(func() {
dashboards.NewService = origNewDashboardService
})
mock := &dashboards.FakeDashboardService{}
dashboards.MockDashboardService(mock)
info, err := pm.ImportDashboard("test-app", "dashboards/connections.json", 1, 0, nil, false,
[]ImportDashboardInput{
{Name: "*", Type: "datasource", Value: "graphite"},
}, &models.SignedInUser{UserId: 1, OrgRole: models.ROLE_ADMIN}, nil)
require.NoError(t, err)
require.NotNil(t, info)
resultStr, err := mock.SavedDashboards[0].Dashboard.Data.EncodePretty()
require.NoError(t, err)
expectedBytes, err := ioutil.ReadFile("testdata/test-app/dashboards/connections_result.json")
require.NoError(t, err)
expectedJson, err := simplejson.NewJson(expectedBytes)
require.NoError(t, err)
expectedStr, err := expectedJson.EncodePretty()
require.NoError(t, err)
require.Equal(t, expectedStr, resultStr)
panel := mock.SavedDashboards[0].Dashboard.Data.Get("rows").GetIndex(0).Get("panels").GetIndex(0)
require.Equal(t, "graphite", panel.Get("datasource").MustString())
})
t.Run("When evaling dashboard template", func(t *testing.T) {
template, err := simplejson.NewJson([]byte(`{
"__inputs": [
{
"name": "DS_NAME",
"type": "datasource"
}
],
"test": {
"prop": "${DS_NAME}_${DS_NAME}"
}
}`))
require.NoError(t, err)
evaluator := &DashTemplateEvaluator{
template: template,
inputs: []ImportDashboardInput{
{Name: "*", Type: "datasource", Value: "my-server"},
},
}
res, err := evaluator.Eval()
require.NoError(t, err)
require.Equal(t, "my-server_my-server", res.GetPath("test", "prop").MustString())
inputs := res.Get("__inputs")
require.Nil(t, inputs.Interface())
})
}
func pluginScenario(t *testing.T, desc string, fn func(*testing.T, *PluginManager)) {
t.Helper()
t.Run("Given a plugin", func(t *testing.T) {
pm := &PluginManager{
Cfg: &setting.Cfg{
FeatureToggles: map[string]bool{},
PluginSettings: setting.PluginSettings{
"test-app": map[string]string{
"path": "testdata/test-app",
},
},
},
}
err := pm.Init()
require.NoError(t, err)
t.Run(desc, func(t *testing.T) {
fn(t, pm)
})
})
}
+101
View File
@@ -0,0 +1,101 @@
package manager
import (
"os"
"path/filepath"
"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"
)
func (pm *PluginManager) GetPluginDashboards(orgId int64, pluginId string) ([]*plugins.PluginDashboardInfoDTO, error) {
plugin, exists := Plugins[pluginId]
if !exists {
return nil, plugins.PluginNotFoundError{PluginID: pluginId}
}
result := make([]*plugins.PluginDashboardInfoDTO, 0)
// load current dashboards
query := models.GetDashboardsByPluginIdQuery{OrgId: orgId, PluginId: pluginId}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
existingMatches := make(map[int64]bool)
for _, include := range plugin.Includes {
if include.Type != plugins.PluginTypeDashboard {
continue
}
dashboard, err := pm.LoadPluginDashboard(plugin.Id, include.Path)
if err != nil {
return nil, err
}
res := &plugins.PluginDashboardInfoDTO{}
res.Path = include.Path
res.PluginId = plugin.Id
res.Title = dashboard.Title
res.Revision = dashboard.Data.Get("revision").MustInt64(1)
// find existing dashboard
for _, existingDash := range query.Result {
if existingDash.Slug == dashboard.Slug {
res.DashboardId = existingDash.Id
res.Imported = true
res.ImportedUri = "db/" + existingDash.Slug
res.ImportedUrl = existingDash.GetUrl()
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
existingMatches[existingDash.Id] = true
}
}
result = append(result, res)
}
// find deleted dashboards
for _, dash := range query.Result {
if _, exists := existingMatches[dash.Id]; !exists {
result = append(result, &plugins.PluginDashboardInfoDTO{
Slug: dash.Slug,
DashboardId: dash.Id,
Removed: true,
})
}
}
return result, nil
}
func (pm *PluginManager) LoadPluginDashboard(pluginId, path string) (*models.Dashboard, error) {
plugin, exists := Plugins[pluginId]
if !exists {
return nil, plugins.PluginNotFoundError{PluginID: pluginId}
}
dashboardFilePath := filepath.Join(plugin.PluginDir, path)
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based
// on plugin folder structure on disk and not user input. `path` comes from the
// `plugin.json` configuration file for the loaded plugin
reader, err := os.Open(dashboardFilePath)
if err != nil {
return nil, err
}
defer func() {
if err := reader.Close(); err != nil {
plog.Warn("Failed to close file", "path", dashboardFilePath, "err", err)
}
}()
data, err := simplejson.NewFromReader(reader)
if err != nil {
return nil, err
}
return models.NewDashboardFromJson(data), nil
}
+67
View File
@@ -0,0 +1,67 @@
package manager
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
func TestPluginDashboards(t *testing.T) {
Convey("When asking for plugin dashboard info", t, func() {
pm := &PluginManager{
Cfg: &setting.Cfg{
FeatureToggles: map[string]bool{},
PluginSettings: setting.PluginSettings{
"test-app": map[string]string{
"path": "testdata/test-app",
},
},
},
}
err := pm.Init()
So(err, ShouldBeNil)
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
if query.Slug == "nginx-connections" {
dash := models.NewDashboard("Nginx Connections")
dash.Data.Set("revision", "1.1")
query.Result = dash
return nil
}
return models.ErrDashboardNotFound
})
bus.AddHandler("test", func(query *models.GetDashboardsByPluginIdQuery) error {
var data = simplejson.New()
data.Set("title", "Nginx Connections")
data.Set("revision", 22)
query.Result = []*models.Dashboard{
{Slug: "nginx-connections", Data: data},
}
return nil
})
dashboards, err := pm.GetPluginDashboards(1, "test-app")
So(err, ShouldBeNil)
Convey("should return 2 dashboards", func() {
So(len(dashboards), ShouldEqual, 2)
})
Convey("should include installed version info", func() {
So(dashboards[0].Title, ShouldEqual, "Nginx Connections")
So(dashboards[0].Revision, ShouldEqual, 25)
So(dashboards[0].ImportedRevision, ShouldEqual, 22)
So(dashboards[0].ImportedUri, ShouldEqual, "db/nginx-connections")
So(dashboards[1].Revision, ShouldEqual, 2)
So(dashboards[1].ImportedRevision, ShouldEqual, 0)
})
})
}
+11
View File
@@ -0,0 +1,11 @@
package manager
import (
"github.com/grafana/grafana/pkg/plugins"
)
const (
signatureMissing plugins.ErrorCode = "signatureMissing"
signatureModified plugins.ErrorCode = "signatureModified"
signatureInvalid plugins.ErrorCode = "signatureInvalid"
)
+616
View File
@@ -0,0 +1,616 @@
// Package manager contains plugin manager logic.
package manager
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"time"
"github.com/grafana/grafana/pkg/infra/fs"
"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/registry"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil"
)
var (
DataSources map[string]*plugins.DataSourcePlugin
Panels map[string]*plugins.PanelPlugin
StaticRoutes []*plugins.PluginStaticRoute
Apps map[string]*plugins.AppPlugin
Plugins map[string]*plugins.PluginBase
PluginTypes map[string]interface{}
Renderer *plugins.RendererPlugin
plog log.Logger
)
type unsignedPluginConditionFunc = func(plugin *plugins.PluginBase) bool
type PluginScanner struct {
pluginPath string
errors []error
backendPluginManager backendplugin.Manager
cfg *setting.Cfg
requireSigned bool
log log.Logger
plugins map[string]*plugins.PluginBase
allowUnsignedPluginsCondition unsignedPluginConditionFunc
}
type PluginManager struct {
BackendPluginManager backendplugin.Manager `inject:""`
Cfg *setting.Cfg `inject:""`
log log.Logger
scanningErrors []error
// AllowUnsignedPluginsCondition changes the policy for allowing unsigned plugins. Signature validation only runs when plugins are starting
// and running plugins will not be terminated if they violate the new policy.
AllowUnsignedPluginsCondition unsignedPluginConditionFunc
GrafanaLatestVersion string
GrafanaHasUpdate bool
pluginScanningErrors map[string]plugins.PluginError
}
func init() {
registry.RegisterService(&PluginManager{})
}
func (pm *PluginManager) Init() error {
pm.log = log.New("plugins")
plog = log.New("plugins")
DataSources = map[string]*plugins.DataSourcePlugin{}
StaticRoutes = []*plugins.PluginStaticRoute{}
Panels = map[string]*plugins.PanelPlugin{}
Apps = map[string]*plugins.AppPlugin{}
Plugins = map[string]*plugins.PluginBase{}
PluginTypes = map[string]interface{}{
"panel": plugins.PanelPlugin{},
"datasource": plugins.DataSourcePlugin{},
"app": plugins.AppPlugin{},
"renderer": plugins.RendererPlugin{},
}
pm.pluginScanningErrors = map[string]plugins.PluginError{}
pm.log.Info("Starting plugin search")
plugDir := filepath.Join(pm.Cfg.StaticRootPath, "app/plugins")
pm.log.Debug("Scanning core plugin directory", "dir", plugDir)
if err := pm.scan(plugDir, false); err != nil {
return errutil.Wrapf(err, "failed to scan core plugin directory '%s'", plugDir)
}
plugDir = pm.Cfg.BundledPluginsPath
pm.log.Debug("Scanning bundled plugins directory", "dir", plugDir)
exists, err := fs.Exists(plugDir)
if err != nil {
return err
}
if exists {
if err := pm.scan(plugDir, false); err != nil {
return errutil.Wrapf(err, "failed to scan bundled plugins directory '%s'", plugDir)
}
}
// check if plugins dir exists
exists, err = fs.Exists(pm.Cfg.PluginsPath)
if err != nil {
return err
}
if !exists {
if err = os.MkdirAll(pm.Cfg.PluginsPath, os.ModePerm); err != nil {
pm.log.Error("failed to create external plugins directory", "dir", pm.Cfg.PluginsPath, "error", err)
} else {
pm.log.Info("External plugins directory created", "directory", pm.Cfg.PluginsPath)
}
} else {
pm.log.Debug("Scanning external plugins directory", "dir", pm.Cfg.PluginsPath)
if err := pm.scan(pm.Cfg.PluginsPath, true); err != nil {
return errutil.Wrapf(err, "failed to scan external plugins directory '%s'",
pm.Cfg.PluginsPath)
}
}
if err := pm.scanPluginPaths(); err != nil {
return err
}
for _, panel := range Panels {
staticRoutes := panel.InitFrontendPlugin()
StaticRoutes = append(StaticRoutes, staticRoutes...)
}
for _, ds := range DataSources {
staticRoutes := ds.InitFrontendPlugin()
StaticRoutes = append(StaticRoutes, staticRoutes...)
}
for _, app := range Apps {
staticRoutes := app.InitApp(Panels, DataSources)
StaticRoutes = append(StaticRoutes, staticRoutes...)
}
if Renderer != nil {
staticRoutes := Renderer.InitFrontendPlugin()
StaticRoutes = append(StaticRoutes, staticRoutes...)
}
for _, p := range Plugins {
if p.IsCorePlugin {
p.Signature = plugins.PluginSignatureInternal
} else {
metrics.SetPluginBuildInformation(p.Id, p.Type, p.Info.Version)
}
}
return nil
}
func (pm *PluginManager) Run(ctx context.Context) error {
pm.checkForUpdates()
ticker := time.NewTicker(time.Minute * 10)
run := true
for run {
select {
case <-ticker.C:
pm.checkForUpdates()
case <-ctx.Done():
run = false
}
}
return ctx.Err()
}
// scanPluginPaths scans configured plugin paths.
func (pm *PluginManager) scanPluginPaths() error {
for pluginID, settings := range pm.Cfg.PluginSettings {
path, exists := settings["path"]
if !exists || path == "" {
continue
}
if err := pm.scan(path, true); err != nil {
return errutil.Wrapf(err, "failed to scan directory configured for plugin '%s': '%s'", pluginID, path)
}
}
return nil
}
// scan a directory for plugins.
func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error {
scanner := &PluginScanner{
pluginPath: pluginDir,
backendPluginManager: pm.BackendPluginManager,
cfg: pm.Cfg,
requireSigned: requireSigned,
log: pm.log,
plugins: map[string]*plugins.PluginBase{},
allowUnsignedPluginsCondition: pm.AllowUnsignedPluginsCondition,
}
// 1st pass: Scan plugins, also mapping plugins to their respective directories
if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil {
if errors.Is(err, os.ErrNotExist) {
pm.log.Debug("Couldn't scan directory since it doesn't exist", "pluginDir", pluginDir, "err", err)
return nil
}
if errors.Is(err, os.ErrPermission) {
pm.log.Debug("Couldn't scan directory due to lack of permissions", "pluginDir", pluginDir, "err", err)
return nil
}
if pluginDir != "data/plugins" {
pm.log.Warn("Could not scan dir", "pluginDir", pluginDir, "err", err)
}
return err
}
pm.log.Debug("Initial plugin loading done")
// 2nd pass: Validate and register plugins
for dpath, plugin := range scanner.plugins {
// Try to find any root plugin
ancestors := strings.Split(dpath, string(filepath.Separator))
ancestors = ancestors[0 : len(ancestors)-1]
aPath := ""
if runtime.GOOS != "windows" && filepath.IsAbs(dpath) {
aPath = "/"
}
for _, a := range ancestors {
aPath = filepath.Join(aPath, a)
if root, ok := scanner.plugins[aPath]; ok {
plugin.Root = root
break
}
}
pm.log.Debug("Found plugin", "id", plugin.Id, "signature", plugin.Signature, "hasRoot", plugin.Root != nil)
signingError := scanner.validateSignature(plugin)
if signingError != nil {
pm.log.Debug("Failed to validate plugin signature. Will skip loading", "id", plugin.Id,
"signature", plugin.Signature, "status", signingError.ErrorCode)
pm.pluginScanningErrors[plugin.Id] = *signingError
continue
}
pm.log.Debug("Attempting to add plugin", "id", plugin.Id)
pluginGoType, exists := PluginTypes[plugin.Type]
if !exists {
return fmt.Errorf("unknown plugin type %q", plugin.Type)
}
jsonFPath := filepath.Join(plugin.PluginDir, "plugin.json")
// External plugins need a module.js file for SystemJS to load
if !strings.HasPrefix(jsonFPath, pm.Cfg.StaticRootPath) && !scanner.IsBackendOnlyPlugin(plugin.Type) {
module := filepath.Join(plugin.PluginDir, "module.js")
exists, err := fs.Exists(module)
if err != nil {
return err
}
if !exists {
scanner.log.Warn("Plugin missing module.js",
"name", plugin.Name,
"warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.",
"path", module)
}
}
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `jsonFPath` is based
// on plugin the folder structure on disk and not user input.
reader, err := os.Open(jsonFPath)
if err != nil {
return err
}
defer func() {
if err := reader.Close(); err != nil {
scanner.log.Warn("Failed to close JSON file", "path", jsonFPath, "err", err)
}
}()
jsonParser := json.NewDecoder(reader)
loader := reflect.New(reflect.TypeOf(pluginGoType)).Interface().(plugins.PluginLoader)
// Load the full plugin, and add it to manager
if err := pm.loadPlugin(jsonParser, plugin, scanner, loader); err != nil {
return err
}
}
if len(scanner.errors) > 0 {
pm.log.Warn("Some plugins failed to load", "errors", scanner.errors)
pm.scanningErrors = scanner.errors
}
return nil
}
func (pm *PluginManager) loadPlugin(jsonParser *json.Decoder, pluginBase *plugins.PluginBase,
scanner *PluginScanner, loader plugins.PluginLoader) error {
plug, err := loader.Load(jsonParser, pluginBase, scanner.backendPluginManager)
if err != nil {
return err
}
var pb *plugins.PluginBase
switch p := plug.(type) {
case *plugins.DataSourcePlugin:
DataSources[p.Id] = p
pb = &p.PluginBase
case *plugins.PanelPlugin:
Panels[p.Id] = p
pb = &p.PluginBase
case *plugins.RendererPlugin:
Renderer = p
pb = &p.PluginBase
case *plugins.AppPlugin:
Apps[p.Id] = p
pb = &p.PluginBase
default:
panic(fmt.Sprintf("Unrecognized plugin type %T", plug))
}
if p, exists := Plugins[pb.Id]; exists {
pm.log.Warn("Plugin is duplicate", "id", pb.Id)
scanner.errors = append(scanner.errors, plugins.DuplicatePluginError{Plugin: pb, ExistingPlugin: p})
return nil
}
if !strings.HasPrefix(pluginBase.PluginDir, pm.Cfg.StaticRootPath) {
pm.log.Info("Registering plugin", "id", pb.Id)
}
if len(pb.Dependencies.Plugins) == 0 {
pb.Dependencies.Plugins = []plugins.PluginDependencyItem{}
}
if pb.Dependencies.GrafanaVersion == "" {
pb.Dependencies.GrafanaVersion = "*"
}
for _, include := range pb.Includes {
if include.Role == "" {
include.Role = models.ROLE_VIEWER
}
}
// Copy relevant fields from the base
pb.PluginDir = pluginBase.PluginDir
pb.Signature = pluginBase.Signature
pb.SignatureType = pluginBase.SignatureType
pb.SignatureOrg = pluginBase.SignatureOrg
Plugins[pb.Id] = pb
pm.log.Debug("Successfully added plugin", "id", pb.Id)
return nil
}
// GetDatasource returns a datasource based on passed pluginID if it exists
//
// This function fetches the datasource from the global variable DataSources in this package.
// Rather then refactor all dependencies on the global variable we can use this as an transition.
func (pm *PluginManager) GetDatasource(pluginID string) (*plugins.DataSourcePlugin, bool) {
ds, exists := DataSources[pluginID]
return ds, exists
}
func (s *PluginScanner) walker(currentPath string, f os.FileInfo, err error) error {
// We scan all the subfolders for plugin.json (with some exceptions) so that we also load embedded plugins, for
// example https://github.com/raintank/worldping-app/tree/master/dist/grafana-worldmap-panel worldmap panel plugin
// is embedded in worldping app.
if err != nil {
return fmt.Errorf("filepath.Walk reported an error for %q: %w", currentPath, err)
}
if f.Name() == "node_modules" || f.Name() == "Chromium.app" {
return util.ErrWalkSkipDir
}
if f.IsDir() {
return nil
}
if f.Name() != "plugin.json" {
return nil
}
if err := s.loadPlugin(currentPath); err != nil {
s.log.Error("Failed to load plugin", "error", err, "pluginPath", filepath.Dir(currentPath))
s.errors = append(s.errors, err)
}
return nil
}
func (s *PluginScanner) loadPlugin(pluginJSONFilePath string) error {
s.log.Debug("Loading plugin", "path", pluginJSONFilePath)
currentDir := filepath.Dir(pluginJSONFilePath)
// 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(pluginJSONFilePath)
if err != nil {
return err
}
defer func() {
if err := reader.Close(); err != nil {
s.log.Warn("Failed to close JSON file", "path", pluginJSONFilePath, "err", err)
}
}()
jsonParser := json.NewDecoder(reader)
pluginCommon := plugins.PluginBase{}
if err := jsonParser.Decode(&pluginCommon); err != nil {
return err
}
if pluginCommon.Id == "" || pluginCommon.Type == "" {
return errors.New("did not find type or id properties in plugin.json")
}
pluginCommon.PluginDir = filepath.Dir(pluginJSONFilePath)
pluginCommon.Files, err = collectPluginFilesWithin(pluginCommon.PluginDir)
if err != nil {
s.log.Warn("Could not collect plugin file information in directory", "pluginID", pluginCommon.Id, "dir", pluginCommon.PluginDir)
return err
}
signatureState, err := getPluginSignatureState(s.log, &pluginCommon)
if err != nil {
s.log.Warn("Could not get plugin signature state", "pluginID", pluginCommon.Id, "err", err)
return err
}
pluginCommon.Signature = signatureState.Status
pluginCommon.SignatureType = signatureState.Type
pluginCommon.SignatureOrg = signatureState.SigningOrg
s.plugins[currentDir] = &pluginCommon
return nil
}
func (*PluginScanner) IsBackendOnlyPlugin(pluginType string) bool {
return pluginType == "renderer"
}
// validateSignature validates a plugin's signature.
func (s *PluginScanner) validateSignature(plugin *plugins.PluginBase) *plugins.PluginError {
if plugin.Signature == plugins.PluginSignatureValid {
s.log.Debug("Plugin has valid signature", "id", plugin.Id)
return nil
}
if plugin.Root != nil {
// If a descendant plugin with invalid signature, set signature to that of root
if plugin.IsCorePlugin || plugin.Signature == plugins.PluginSignatureInternal {
s.log.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 {
s.log.Debug("Setting descendant plugin's signature to that of root", "plugin", plugin.Id,
"root", plugin.Root.Id, "signature", plugin.Signature, "rootSignature", plugin.Root.Signature)
plugin.Signature = plugin.Root.Signature
if plugin.Signature == plugins.PluginSignatureValid {
s.log.Debug("Plugin has valid signature (inherited from root)", "id", plugin.Id)
return nil
}
}
} else {
s.log.Debug("Non-valid plugin Signature", "pluginID", plugin.Id, "pluginDir", plugin.PluginDir,
"state", plugin.Signature)
}
// For the time being, we choose to only require back-end plugins to be signed
// NOTE: the state is calculated again when setting metadata on the object
if !plugin.Backend || !s.requireSigned {
return nil
}
switch plugin.Signature {
case plugins.PluginSignatureUnsigned:
if allowed := s.allowUnsigned(plugin); !allowed {
s.log.Debug("Plugin is unsigned", "id", plugin.Id)
s.errors = append(s.errors, fmt.Errorf("plugin %q is unsigned", plugin.Id))
return &plugins.PluginError{
ErrorCode: signatureMissing,
}
}
s.log.Warn("Running an unsigned backend plugin", "pluginID", plugin.Id, "pluginDir",
plugin.PluginDir)
return nil
case plugins.PluginSignatureInvalid:
s.log.Debug("Plugin %q has an invalid signature", plugin.Id)
s.errors = append(s.errors, fmt.Errorf("plugin %q has an invalid signature", plugin.Id))
return &plugins.PluginError{
ErrorCode: signatureInvalid,
}
case plugins.PluginSignatureModified:
s.log.Debug("Plugin %q has a modified signature", plugin.Id)
s.errors = append(s.errors, fmt.Errorf("plugin %q's signature has been modified", plugin.Id))
return &plugins.PluginError{
ErrorCode: signatureModified,
}
default:
panic(fmt.Sprintf("Plugin %q has unrecognized plugin signature state %q", plugin.Id, plugin.Signature))
}
}
func (s *PluginScanner) allowUnsigned(plugin *plugins.PluginBase) bool {
if s.allowUnsignedPluginsCondition != nil {
return s.allowUnsignedPluginsCondition(plugin)
}
if s.cfg.Env == setting.Dev {
return true
}
for _, plug := range s.cfg.PluginsAllowUnsigned {
if plug == plugin.Id {
return true
}
}
return false
}
// ScanningErrors returns plugin scanning errors encountered.
func (pm *PluginManager) ScanningErrors() []plugins.PluginError {
scanningErrs := make([]plugins.PluginError, 0)
for id, e := range pm.pluginScanningErrors {
scanningErrs = append(scanningErrs, plugins.PluginError{
ErrorCode: e.ErrorCode,
PluginID: id,
})
}
return scanningErrs
}
func (pm *PluginManager) GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
plug, exists := Plugins[pluginId]
if !exists {
return nil, plugins.PluginNotFoundError{PluginID: pluginId}
}
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based
// on plugin the folder structure on disk and not user input.
path := filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
exists, err := fs.Exists(path)
if err != nil {
return nil, err
}
if !exists {
path = filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name)))
}
exists, err = fs.Exists(path)
if err != nil {
return nil, err
}
if !exists {
return make([]byte, 0), nil
}
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based
// on plugin the folder structure on disk and not user input.
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return data, nil
}
// gets plugin filenames that require verification for plugin signing
func collectPluginFilesWithin(rootDir string) ([]string, error) {
var files []string
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && info.Name() != "MANIFEST.txt" {
file, err := filepath.Rel(rootDir, path)
if err != nil {
return err
}
files = append(files, filepath.ToSlash(file))
}
return nil
})
return files, err
}
// GetDataPlugin gets a DataPlugin with a certain name. If none is found, nil is returned.
func (pm *PluginManager) GetDataPlugin(pluginID string) plugins.DataPlugin {
if p, exists := DataSources[pluginID]; exists && p.CanHandleDataQueries() {
return p
}
// XXX: Might other plugins implement DataPlugin?
p := pm.BackendPluginManager.GetDataPlugin(pluginID)
if p != nil {
return p.(plugins.DataPlugin)
}
return nil
}
+293
View File
@@ -0,0 +1,293 @@
package manager
import (
"context"
"errors"
"fmt"
"path/filepath"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"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"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
)
func TestPluginManager_Init(t *testing.T) {
staticRootPath, err := filepath.Abs("../../../public/")
require.NoError(t, err)
origRootPath := setting.StaticRootPath
origRaw := setting.Raw
origEnv := setting.Env
t.Cleanup(func() {
setting.StaticRootPath = origRootPath
setting.Raw = origRaw
setting.Env = origEnv
})
setting.StaticRootPath = staticRootPath
setting.Raw = ini.Empty()
setting.Env = setting.Prod
t.Run("Base case", func(t *testing.T) {
pm := &PluginManager{
Cfg: &setting.Cfg{
Raw: ini.Empty(),
Env: setting.Prod,
StaticRootPath: staticRootPath,
PluginSettings: setting.PluginSettings{
"nginx-app": map[string]string{
"path": "testdata/test-app",
},
},
},
}
err := pm.Init()
require.NoError(t, err)
assert.Empty(t, pm.scanningErrors)
assert.Greater(t, len(DataSources), 1)
assert.Greater(t, len(Panels), 1)
assert.Equal(t, "app/plugins/datasource/graphite/module", DataSources["graphite"].Module)
assert.NotEmpty(t, Apps)
assert.Equal(t, "public/plugins/test-app/img/logo_large.png", Apps["test-app"].Info.Logos.Large)
assert.Equal(t, "public/plugins/test-app/img/screenshot2.png", Apps["test-app"].Info.Screenshots[1].Path)
})
t.Run("With external back-end plugin lacking signature", func(t *testing.T) {
pm := &PluginManager{
Cfg: &setting.Cfg{PluginsPath: "testdata/unsigned"},
}
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test" is unsigned`)}, pm.scanningErrors)
})
t.Run("With external unsigned back-end plugin and configuration disabling signature check of this plugin", func(t *testing.T) {
pm := &PluginManager{
Cfg: &setting.Cfg{
PluginsPath: "testdata/unsigned",
PluginsAllowUnsigned: []string{"test"},
},
BackendPluginManager: &fakeBackendPluginManager{},
}
err := pm.Init()
require.NoError(t, err)
assert.Empty(t, pm.scanningErrors)
})
t.Run("With external back-end plugin with invalid v1 signature", func(t *testing.T) {
pm := &PluginManager{
Cfg: &setting.Cfg{
PluginsPath: "testdata/invalid-v1-signature",
},
}
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test" has an invalid signature`)}, pm.scanningErrors)
})
t.Run("With external back-end plugin lacking files listed in manifest", func(t *testing.T) {
fm := &fakeBackendPluginManager{}
pm := &PluginManager{
Cfg: &setting.Cfg{
PluginsPath: "testdata/lacking-files",
},
BackendPluginManager: fm,
}
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors)
})
t.Run("Transform plugins should be ignored when expressions feature is off", func(t *testing.T) {
fm := fakeBackendPluginManager{}
pm := &PluginManager{
Cfg: &setting.Cfg{
PluginsPath: "testdata/behind-feature-flag",
},
BackendPluginManager: &fm,
}
err := pm.Init()
require.NoError(t, err)
assert.Empty(t, pm.scanningErrors)
assert.Empty(t, fm.registeredPlugins)
})
t.Run("With nested plugin duplicating parent", func(t *testing.T) {
pm := &PluginManager{
Cfg: &setting.Cfg{
PluginsPath: "testdata/duplicate-plugins",
},
}
err := pm.Init()
require.NoError(t, err)
assert.Len(t, pm.scanningErrors, 1)
assert.True(t, errors.Is(pm.scanningErrors[0], plugins.DuplicatePluginError{}))
})
t.Run("With external back-end plugin with valid v2 signature", func(t *testing.T) {
pm := &PluginManager{
Cfg: &setting.Cfg{
PluginsPath: "testdata/valid-v2-signature",
},
BackendPluginManager: &fakeBackendPluginManager{},
}
err := pm.Init()
require.NoError(t, err)
require.Empty(t, pm.scanningErrors)
pluginId := "test"
assert.NotNil(t, Plugins[pluginId])
assert.Equal(t, "datasource", Plugins[pluginId].Type)
assert.Equal(t, "Test", Plugins[pluginId].Name)
assert.Equal(t, pluginId, Plugins[pluginId].Id)
assert.Equal(t, "1.0.0", Plugins[pluginId].Info.Version)
assert.Equal(t, plugins.PluginSignatureValid, Plugins[pluginId].Signature)
assert.Equal(t, plugins.GrafanaType, Plugins[pluginId].SignatureType)
assert.Equal(t, "Grafana Labs", Plugins[pluginId].SignatureOrg)
assert.False(t, Plugins[pluginId].IsCorePlugin)
})
t.Run("With back-end plugin with invalid v2 private signature (mismatched root URL)", func(t *testing.T) {
origAppURL := setting.AppUrl
t.Cleanup(func() {
setting.AppUrl = origAppURL
})
setting.AppUrl = "http://localhost:1234"
pm := &PluginManager{
Cfg: &setting.Cfg{
PluginsPath: "testdata/valid-v2-pvt-signature",
},
}
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test" has an invalid signature`)}, pm.scanningErrors)
assert.Nil(t, Plugins[("test")])
})
t.Run("With back-end plugin with valid v2 private signature", func(t *testing.T) {
origAppURL := setting.AppUrl
t.Cleanup(func() {
setting.AppUrl = origAppURL
})
setting.AppUrl = "http://localhost:3000/"
pm := &PluginManager{
Cfg: &setting.Cfg{
PluginsPath: "testdata/valid-v2-pvt-signature",
},
BackendPluginManager: &fakeBackendPluginManager{},
}
err := pm.Init()
require.NoError(t, err)
require.Empty(t, pm.scanningErrors)
pluginId := "test"
assert.NotNil(t, Plugins[pluginId])
assert.Equal(t, "datasource", Plugins[pluginId].Type)
assert.Equal(t, "Test", Plugins[pluginId].Name)
assert.Equal(t, pluginId, Plugins[pluginId].Id)
assert.Equal(t, "1.0.0", Plugins[pluginId].Info.Version)
assert.Equal(t, plugins.PluginSignatureValid, Plugins[pluginId].Signature)
assert.Equal(t, plugins.PrivateType, Plugins[pluginId].SignatureType)
assert.Equal(t, "Will Browne", Plugins[pluginId].SignatureOrg)
assert.False(t, Plugins[pluginId].IsCorePlugin)
})
t.Run("With back-end plugin with modified v2 signature (missing file from plugin dir)", func(t *testing.T) {
origAppURL := setting.AppUrl
t.Cleanup(func() {
setting.AppUrl = origAppURL
})
setting.AppUrl = "http://localhost:3000/"
pm := &PluginManager{
Cfg: &setting.Cfg{
PluginsPath: "testdata/invalid-v2-signature",
},
BackendPluginManager: &fakeBackendPluginManager{},
}
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors)
assert.Nil(t, Plugins[("test")])
})
t.Run("With back-end plugin with modified v2 signature (unaccounted file in plugin dir)", func(t *testing.T) {
origAppURL := setting.AppUrl
t.Cleanup(func() {
setting.AppUrl = origAppURL
})
setting.AppUrl = "http://localhost:3000/"
pm := &PluginManager{
Cfg: &setting.Cfg{
PluginsPath: "testdata/invalid-v2-signature-2",
},
BackendPluginManager: &fakeBackendPluginManager{},
}
err := pm.Init()
require.NoError(t, err)
assert.Equal(t, []error{fmt.Errorf(`plugin "test"'s signature has been modified`)}, pm.scanningErrors)
assert.Nil(t, Plugins[("test")])
})
}
func TestPluginManager_IsBackendOnlyPlugin(t *testing.T) {
pluginScanner := &PluginScanner{}
type testCase struct {
name string
isBackendOnly bool
}
for _, c := range []testCase{
{name: "renderer", isBackendOnly: true},
{name: "app", isBackendOnly: false},
} {
t.Run(fmt.Sprintf("Plugin %s", c.name), func(t *testing.T) {
result := pluginScanner.IsBackendOnlyPlugin(c.name)
assert.Equal(t, c.isBackendOnly, result)
})
}
}
type fakeBackendPluginManager struct {
backendplugin.Manager
registeredPlugins []string
}
func (f *fakeBackendPluginManager) Register(pluginID string, factory backendplugin.PluginFactoryFunc) error {
f.registeredPlugins = append(f.registeredPlugins, pluginID)
return nil
}
func (f *fakeBackendPluginManager) StartPlugin(ctx context.Context, pluginID string) error {
return nil
}
func (f *fakeBackendPluginManager) CollectMetrics(ctx context.Context, pluginID string) (*backend.CollectMetricsResult, error) {
return nil, nil
}
func (f *fakeBackendPluginManager) CheckHealth(ctx context.Context, pCtx backend.PluginContext) (*backend.CheckHealthResult, error) {
return nil, nil
}
func (f *fakeBackendPluginManager) CallResource(pluginConfig backend.PluginContext, ctx *models.ReqContext, path string) {
}
+227
View File
@@ -0,0 +1,227 @@
package manager
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
"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:
// https://grafana.com/api/plugins/ci/keys
const publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
xpMEXpTXXxMFK4EEACMEIwQBiOUQhvGbDLvndE0fEXaR0908wXzPGFpf0P0Z
HJ06tsq+0higIYHp7WTNJVEZtcwoYLcPRGaa9OQqbUU63BEyZdgAkPTz3RFd
5+TkDWZizDcaVFhzbDd500yTwexrpIrdInwC/jrgs7Zy/15h8KA59XXUkdmT
YB6TR+OA9RKME+dCJozNGUdyYWZhbmEgPGVuZ0BncmFmYW5hLmNvbT7CvAQQ
EwoAIAUCXpTXXwYLCQcIAwIEFQgKAgQWAgEAAhkBAhsDAh4BAAoJEH5NDGpw
iGbnaWoCCQGQ3SQnCkRWrG6XrMkXOKfDTX2ow9fuoErN46BeKmLM4f1EkDZQ
Tpq3SE8+My8B5BIH3SOcBeKzi3S57JHGBdFA+wIJAYWMrJNIvw8GeXne+oUo
NzzACdvfqXAZEp/HFMQhCKfEoWGJE8d2YmwY2+3GufVRTI5lQnZOHLE8L/Vc
1S5MXESjzpcEXpTXXxIFK4EEACMEIwQBtHX/SD5Qm3v4V92qpaIZQgtTX0sT
cFPjYWAHqsQ1iENrYN/vg1wU3ADlYATvydOQYvkTyT/tbDvx2Fse8PL84MQA
YKKQ6AJ3gLVvmeouZdU03YoV4MYaT8KbnJUkZQZkqdz2riOlySNI9CG3oYmv
omjUAtzgAgnCcurfGLZkkMxlmY8DAQoJwqQEGBMKAAkFAl6U118CGwwACgkQ
fk0ManCIZuc0jAIJAVw2xdLr4ZQqPUhubrUyFcqlWoW8dQoQagwO8s8ubmby
KuLA9FWJkfuuRQr+O9gHkDVCez3aism7zmJBqIOi38aNAgjJ3bo6leSS2jR/
x5NqiKVi83tiXDPncDQYPymOnMhW0l7CVA7wj75HrFvvlRI/4MArlbsZ2tBn
N1c5v9v/4h6qeA==
=DNbR
-----END PGP PUBLIC KEY BLOCK-----
`
// pluginManifest holds details for the file manifest
type pluginManifest struct {
Plugin string `json:"plugin"`
Version string `json:"version"`
KeyID string `json:"keyId"`
Time int64 `json:"time"`
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"`
}
func (m *pluginManifest) isV2() bool {
return strings.HasPrefix(m.ManifestVersion, "2.")
}
// readPluginManifest attempts to read and verify the plugin manifest
// if any error occurs or the manifest is not valid, this will return an error
func readPluginManifest(body []byte) (*pluginManifest, error) {
block, _ := clearsign.Decode(body)
if block == nil {
return nil, errors.New("unable to decode manifest")
}
// Convert to a well typed object
manifest := &pluginManifest{}
err := json.Unmarshal(block.Plaintext, &manifest)
if err != nil {
return nil, errutil.Wrap("Error parsing manifest JSON", err)
}
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewBufferString(publicKeyText))
if err != nil {
return nil, errutil.Wrap("failed to parse public key", err)
}
if _, err := openpgp.CheckDetachedSignature(keyring,
bytes.NewBuffer(block.Bytes),
block.ArmoredSignature.Body); err != nil {
return nil, errutil.Wrap("failed to check signature", err)
}
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)
manifestPath := filepath.Join(plugin.PluginDir, "MANIFEST.txt")
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `manifestPath` is based
// 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,
}, nil
}
manifest, err := readPluginManifest(byteValue)
if err != nil {
log.Debug("Plugin signature invalid", "id", plugin.Id)
return plugins.PluginSignatureState{
Status: plugins.PluginSignatureInvalid,
}, nil
}
// Make sure the versions all match
if manifest.Plugin != plugin.Id || manifest.Version != plugin.Info.Version {
return plugins.PluginSignatureState{
Status: plugins.PluginSignatureModified,
}, nil
}
// Validate that private is running within defined root URLs
if manifest.SignatureType == plugins.PrivateType {
appURL, err := url.Parse(setting.AppUrl)
if err != nil {
return plugins.PluginSignatureState{}, err
}
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
}
if rootURL.Scheme == appURL.Scheme &&
rootURL.Host == appURL.Host &&
rootURL.RequestURI() == appURL.RequestURI() {
foundMatch = true
break
}
}
if !foundMatch {
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,
}, nil
}
}
manifestFiles := make(map[string]bool, len(manifest.Files))
// Verify the manifest contents
log.Debug("Verifying contents of plugin manifest", "plugin", plugin.Id)
for p, hash := range manifest.Files {
// Open the file
fp := filepath.Join(plugin.PluginDir, p)
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `fp` is based
// on the manifest file for a plugin and not user input.
f, err := os.Open(fp)
if err != nil {
log.Warn("Plugin file listed in the manifest was not found", "plugin", plugin.Id, "filename", p, "dir", plugin.PluginDir)
return plugins.PluginSignatureState{
Status: plugins.PluginSignatureModified,
}, nil
}
defer func() {
if err := f.Close(); err != nil {
log.Warn("Failed to close plugin file", "path", fp, "err", err)
}
}()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
log.Warn("Couldn't read plugin file", "plugin", plugin.Id, "filename", fp)
return plugins.PluginSignatureState{
Status: plugins.PluginSignatureModified,
}, nil
}
sum := hex.EncodeToString(h.Sum(nil))
if sum != hash {
log.Warn("Plugin file's signature has been modified versus manifest", "plugin", plugin.Id, "filename", fp)
return plugins.PluginSignatureState{
Status: plugins.PluginSignatureModified,
}, nil
}
manifestFiles[p] = true
}
if manifest.isV2() {
// Track files missing from the manifest
var unsignedFiles []string
for _, f := range plugin.Files {
if _, exists := manifestFiles[f]; !exists {
unsignedFiles = append(unsignedFiles, f)
}
}
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,
}, nil
}
}
// Everything OK
log.Debug("Plugin signature valid", "id", plugin.Id)
return plugins.PluginSignatureState{
Status: plugins.PluginSignatureValid,
Type: manifest.SignatureType,
SigningOrg: manifest.SignedByOrgName,
}, nil
}
+123
View File
@@ -0,0 +1,123 @@
package manager
import (
"sort"
"strings"
"testing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReadPluginManifest(t *testing.T) {
txt := `-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{
"plugin": "grafana-googlesheets-datasource",
"version": "1.0.0-dev",
"files": {
"LICENSE": "7df059597099bb7dcf25d2a9aedfaf4465f72d8d",
"README.md": "08ec6d704b6115bef57710f6d7e866c050cb50ee",
"gfx_sheets_darwin_amd64": "1b8ae92c6e80e502bb0bf2d0ae9d7223805993ab",
"gfx_sheets_linux_amd64": "f39e0cc7344d3186b1052e6d356eecaf54d75b49",
"gfx_sheets_windows_amd64.exe": "c8825dfec512c1c235244f7998ee95182f9968de",
"module.js": "aaec6f51a995b7b843b843cd14041925274d960d",
"module.js.LICENSE.txt": "7f822fe9341af8f82ad1b0c69aba957822a377cf",
"module.js.map": "c5a524f5c4237f6ed6a016d43cd46938efeadb45",
"plugin.json": "55556b845e91935cc48fae3aa67baf0f22694c3f"
},
"time": 1586817677115,
"keyId": "7e4d0c6a708866e7"
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
wqEEARMKAAYFAl6U6o0ACgkQfk0ManCIZuevWAIHSvcxOy1SvvL5gC+HpYyG
VbSsUvF2FsCoXUCTQflK6VdJfSPNzm8YdCdx7gNrBdly6HEs06ZaRp44F/ve
NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
74uuP5UXZxwK2YKJovdSknDIU7MhfuvvQIP/og==
=hBea
-----END PGP SIGNATURE-----`
t.Run("valid manifest", func(t *testing.T) {
manifest, err := readPluginManifest([]byte(txt))
require.NoError(t, err)
require.NotNil(t, manifest)
assert.Equal(t, "grafana-googlesheets-datasource", manifest.Plugin)
assert.Equal(t, "1.0.0-dev", manifest.Version)
assert.Equal(t, int64(1586817677115), manifest.Time)
assert.Equal(t, "7e4d0c6a708866e7", manifest.KeyID)
expectedFiles := []string{"LICENSE", "README.md", "gfx_sheets_darwin_amd64", "gfx_sheets_linux_amd64",
"gfx_sheets_windows_amd64.exe", "module.js", "module.js.LICENSE.txt", "module.js.map", "plugin.json",
}
assert.Equal(t, expectedFiles, fileList(manifest))
})
t.Run("invalid manifest", func(t *testing.T) {
modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx")
_, err := readPluginManifest([]byte(modified))
require.Error(t, err)
})
}
func TestReadPluginManifestV2(t *testing.T) {
txt := `-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{
"manifestVersion": "2.0.0",
"signatureType": "private",
"signedByOrg": "willbrowne",
"signedByOrgName": "Will Browne",
"rootUrls": [
"http://localhost:3000/"
],
"plugin": "test",
"version": "1.0.0",
"time": 1605807018050,
"keyId": "7e4d0c6a708866e7",
"files": {
"plugin.json": "2bb467c0bfd6c454551419efe475b8bf8573734e73c7bab52b14842adb62886f"
}
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
wqIEARMKAAYFAl+2q6oACgkQfk0ManCIZudmzwIJAXWz58cd/91rTXszKPnE
xbVEvERCbjKTtPBQBNQyqEvV+Ig3MuBSNOVy2SOGrMsdbS6lONgvgt4Cm+iS
wV+vYifkAgkBJtg/9DMB7/iX5O0h49CtSltcpfBFXlGqIeOwRac/yENzRzAA
khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
=rLIE
-----END PGP SIGNATURE-----`
t.Run("valid manifest", func(t *testing.T) {
manifest, err := readPluginManifest([]byte(txt))
require.NoError(t, err)
require.NotNil(t, manifest)
assert.Equal(t, "test", manifest.Plugin)
assert.Equal(t, "1.0.0", manifest.Version)
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, "willbrowne", manifest.SignedByOrg)
assert.Equal(t, "Will Browne", manifest.SignedByOrgName)
assert.Equal(t, []string{"http://localhost:3000/"}, manifest.RootURLs)
assert.Equal(t, []string{"plugin.json"}, fileList(manifest))
})
}
func fileList(manifest *pluginManifest) []string {
var keys []string
for k := range manifest.Files {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
+94
View File
@@ -0,0 +1,94 @@
package manager
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
)
func (pm *PluginManager) GetPluginSettings(orgId int64) (map[string]*models.PluginSettingInfoDTO, error) {
query := models.GetPluginSettingsQuery{OrgId: orgId}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
pluginMap := make(map[string]*models.PluginSettingInfoDTO)
for _, plug := range query.Result {
pluginMap[plug.PluginId] = plug
}
for _, pluginDef := range 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 := 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 pluginID, app := range Apps {
if b, ok := pluginSettingMap[pluginID]; 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 DataSources {
if _, exists := pluginSettingMap[ds.Id]; exists {
enabledPlugins.DataSources[dsID] = ds
}
}
for _, panel := range 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 IsAppInstalled(pluginID string) bool {
_, exists := Apps[pluginID]
return exists
}
@@ -0,0 +1,14 @@
{
"type": "datasource",
"name": "Child",
"id": "test-app",
"info": {
"description": "Child plugin",
"author": {
"name": "Grafana Labs",
"url": "http://grafana.com"
},
"version": "1.0.0",
"updated": "2020-10-20"
}
}
@@ -0,0 +1,14 @@
{
"type": "datasource",
"name": "Parent",
"id": "test-app",
"info": {
"description": "Parent plugin",
"author": {
"name": "Grafana Labs",
"url": "http://grafana.com"
},
"version": "1.0.0",
"updated": "2020-10-20"
}
}
@@ -0,0 +1 @@
Invalid manifest
@@ -0,0 +1,14 @@
{
"type": "datasource",
"name": "Test",
"id": "test",
"backend": true,
"state": "alpha",
"info": {
"description": "Test",
"author": {
"name": "Grafana Labs",
"url": "https://grafana.com"
}
}
}
@@ -0,0 +1,27 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{
"manifestVersion": "2.0.0",
"signatureType": "grafana",
"signedByOrg": "grafana",
"signedByOrgName": "Grafana Labs",
"plugin": "test",
"version": "1.0.0",
"time": 1605807330546,
"keyId": "7e4d0c6a708866e7",
"files": {
"plugin.json": "2bb467c0bfd6c454551419efe475b8bf8573734e73c7bab52b14842adb62886f"
}
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
wqEEARMKAAYFAl+2rOIACgkQfk0ManCIZudNOwIJAT8FTzwnRFCSLTOaR3F3
2Fh96eRbghokXcQG9WqpQAg8ZiVfGXeWWRNtV+nuQ9VOZOTO0BovWLuMkym2
ci8ABpWOAgd46LkGn3Dd8XVnGmLI6UPqHAXflItOrCMRiGcYJn5PxP1aCz8h
D0JoNI9TIKrhMtM4voU3Qhf3mIOTHueuDNS48w==
=mu2j
-----END PGP SIGNATURE-----
@@ -0,0 +1,16 @@
{
"type": "datasource",
"name": "Test",
"id": "test",
"backend": true,
"executable": "test",
"state": "alpha",
"info": {
"version": "1.0.0",
"description": "Test",
"author": {
"name": "Will Browne",
"url": "https://willbrowne.com"
}
}
}
@@ -0,0 +1,28 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{
"manifestVersion": "2.0.0",
"signatureType": "grafana",
"signedByOrg": "grafana",
"signedByOrgName": "Grafana Labs",
"plugin": "test",
"version": "1.0.0",
"time": 1605809299800,
"keyId": "7e4d0c6a708866e7",
"files": {
"plugin.json": "2bb467c0bfd6c454551419efe475b8bf8573734e73c7bab52b14842adb62886f",
"veryImportantFile": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
wqIEARMKAAYFAl+2tJMACgkQfk0ManCIZueB0AIJAT/PWs226MaIu3eDZy4o
3UH/tIExyY4zR+VSBfTS+Gji5BcIRkIn7bhM1U40KDraDCvQOl3WetgqQkPd
wcSTJJocAgkBrsrxNz/Nl+vw/usre3Funj0hPVS/6NnJXwe6sVH+gAQfeddz
MzYTY/gcUVWp8Y7l/Hg44nry0PS3sr5LQ30w/FY=
=ev+T
-----END PGP SIGNATURE-----
@@ -0,0 +1,16 @@
{
"type": "datasource",
"name": "Test",
"id": "test",
"backend": true,
"executable": "test",
"state": "alpha",
"info": {
"version": "1.0.0",
"description": "Test",
"author": {
"name": "Will Browne",
"url": "https://willbrowne.com"
}
}
}
@@ -0,0 +1,24 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{
"plugin": "test",
"version": "1.0.0",
"files": {
"executable": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"plugin.json": "f19e18ca5dec690b94f2ff7866372db4bc63f4ff1101ee5d9061b9a6f026f6dd"
},
"time": 1589270570251,
"keyId": "7e4d0c6a708866e7"
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
wqIEARMKAAYFAl66WCoACgkQfk0ManCIZuf8zwIJAWBVDJyzqchZ/DN7ZDCy
Kyb63CajW/XdgHalPDB0sZ/80ExjCSprkFYPL+hUlTUHIXh+jGkfnYpMoqWA
Om77bFc2AgkBV8HTsuRw/lXUezKnDuXcgUIvHvEwKWTvtbLgcuMXMDAVAEBj
isBWmA8xvUfMzgQ9CJUHAaJ6hf1erpE4BuBqtMM=
=uBIX
-----END PGP SIGNATURE-----
@@ -0,0 +1,14 @@
{
"type": "datasource",
"name": "Test",
"id": "test",
"backend": true,
"info": {
"description": "Test",
"version": "1.0.0",
"author": {
"name": "Grafana Labs",
"url": "https://grafana.com"
}
}
}
@@ -0,0 +1,29 @@
{
"__inputs": [
{
"name": "DS_NAME",
"type": "datasource",
"pluginId": "graphite"
}
],
"uid": "1MHHlVjzz",
"title": "Nginx Connections",
"revision": 25,
"schemaVersion": 11,
"tags": ["tag1", "tag2"],
"number_array": [1,2,3,10.33],
"boolean_true": true,
"boolean_false": false,
"rows": [
{
"panels": [
{
"type": "graph",
"datasource": "${DS_NAME}"
}
]
}
]
}
@@ -0,0 +1,21 @@
{
"revision": 25,
"tags": ["tag1", "tag2"],
"boolean_false": false,
"boolean_true": true,
"number_array": [1,2,3,10.33],
"rows": [
{
"panels": [
{
"datasource": "graphite",
"type": "graph"
}
]
}
],
"schemaVersion": 11,
"title": "Nginx Connections",
"uid": "1MHHlVjzz",
"version": 0
}
@@ -0,0 +1,5 @@
{
"title": "Nginx Memory",
"revision": 2,
"schemaVersion": 11
}
+55
View File
@@ -0,0 +1,55 @@
{
"type": "app",
"name": "Test App",
"id": "test-app",
"staticRoot": ".",
"pages": [
{ "name": "Live stream", "component": "StreamPageCtrl", "role": "Editor"},
{ "name": "Log view", "component": "LogsPageCtrl", "role": "Viewer"}
],
"css": {
"dark": "css/dark.css",
"light": "css/light.css"
},
"info": {
"description": "Official Grafana Test App & Dashboard bundle",
"author": {
"name": "Test Inc.",
"url": "http://test.com"
},
"keywords": ["test"],
"logos": {
"small": "img/logo_small.png",
"large": "img/logo_large.png"
},
"screenshots": [
{"name": "img1", "path": "img/screenshot1.png"},
{"name": "img2", "path": "img/screenshot2.png"}
],
"links": [
{"name": "Project site", "url": "http://project.com"},
{"name": "License & Terms", "url": "http://license.com"}
],
"version": "1.0.0",
"updated": "2015-02-10"
},
"includes": [
{"type": "dashboard", "name": "Nginx Connections", "path": "dashboards/connections.json"},
{"type": "dashboard", "name": "Nginx Memory", "path": "dashboards/memory.json"},
{"type": "panel", "name": "Nginx Panel"},
{"type": "datasource", "name": "Nginx Datasource"}
],
"dependencies": {
"grafanaVersion": "3.x.x",
"plugins": [
{"type": "datasource", "id": "graphite", "name": "Graphite", "version": "1.0.0"},
{"type": "panel", "id": "graph", "name": "Graph", "version": "1.0.0"}
]
}
}
@@ -0,0 +1,14 @@
{
"type": "datasource",
"name": "Test",
"id": "test",
"backend": true,
"state": "alpha",
"info": {
"description": "Test",
"author": {
"name": "Grafana Labs",
"url": "https://grafana.com"
}
}
}
@@ -0,0 +1,30 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{
"manifestVersion": "2.0.0",
"signatureType": "private",
"signedByOrg": "willbrowne",
"signedByOrgName": "Will Browne",
"rootUrls": [
"http://localhost:3000/"
],
"plugin": "test",
"version": "1.0.0",
"time": 1605807018050,
"keyId": "7e4d0c6a708866e7",
"files": {
"plugin.json": "2bb467c0bfd6c454551419efe475b8bf8573734e73c7bab52b14842adb62886f"
}
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
wqIEARMKAAYFAl+2q6oACgkQfk0ManCIZudmzwIJAXWz58cd/91rTXszKPnE
xbVEvERCbjKTtPBQBNQyqEvV+Ig3MuBSNOVy2SOGrMsdbS6lONgvgt4Cm+iS
wV+vYifkAgkBJtg/9DMB7/iX5O0h49CtSltcpfBFXlGqIeOwRac/yENzRzAA
khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
=rLIE
-----END PGP SIGNATURE-----
@@ -0,0 +1,16 @@
{
"type": "datasource",
"name": "Test",
"id": "test",
"backend": true,
"executable": "test",
"state": "alpha",
"info": {
"version": "1.0.0",
"description": "Test",
"author": {
"name": "Will Browne",
"url": "https://willbrowne.com"
}
}
}
@@ -0,0 +1,27 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
{
"manifestVersion": "2.0.0",
"signatureType": "grafana",
"signedByOrg": "grafana",
"signedByOrgName": "Grafana Labs",
"plugin": "test",
"version": "1.0.0",
"time": 1605807330546,
"keyId": "7e4d0c6a708866e7",
"files": {
"plugin.json": "2bb467c0bfd6c454551419efe475b8bf8573734e73c7bab52b14842adb62886f"
}
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org
wqEEARMKAAYFAl+2rOIACgkQfk0ManCIZudNOwIJAT8FTzwnRFCSLTOaR3F3
2Fh96eRbghokXcQG9WqpQAg8ZiVfGXeWWRNtV+nuQ9VOZOTO0BovWLuMkym2
ci8ABpWOAgd46LkGn3Dd8XVnGmLI6UPqHAXflItOrCMRiGcYJn5PxP1aCz8h
D0JoNI9TIKrhMtM4voU3Qhf3mIOTHueuDNS48w==
=mu2j
-----END PGP SIGNATURE-----
@@ -0,0 +1,16 @@
{
"type": "datasource",
"name": "Test",
"id": "test",
"backend": true,
"executable": "test",
"state": "alpha",
"info": {
"version": "1.0.0",
"description": "Test",
"author": {
"name": "Will Browne",
"url": "https://willbrowne.com"
}
}
}
+127
View File
@@ -0,0 +1,127 @@
package manager
import (
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/hashicorp/go-version"
)
var (
httpClient = http.Client{Timeout: 10 * time.Second}
)
type GrafanaNetPlugin struct {
Slug string `json:"slug"`
Version string `json:"version"`
}
type GithubLatest struct {
Stable string `json:"stable"`
Testing string `json:"testing"`
}
func getAllExternalPluginSlugs() string {
var result []string
for _, plug := range Plugins {
if plug.IsCorePlugin {
continue
}
result = append(result, plug.Id)
}
return strings.Join(result, ",")
}
func (pm *PluginManager) checkForUpdates() {
if !pm.Cfg.CheckForUpdates {
return
}
pm.log.Debug("Checking for updates")
pluginSlugs := getAllExternalPluginSlugs()
resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
if err != nil {
log.Tracef("Failed to get plugins repo from grafana.com, %v", err.Error())
return
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Warn("Failed to close response body", "err", err)
}
}()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Tracef("Update check failed, reading response from grafana.com, %v", err.Error())
return
}
gNetPlugins := []GrafanaNetPlugin{}
err = json.Unmarshal(body, &gNetPlugins)
if err != nil {
log.Tracef("Failed to unmarshal plugin repo, reading response from grafana.com, %v", err.Error())
return
}
for _, plug := range Plugins {
for _, gplug := range gNetPlugins {
if gplug.Slug == plug.Id {
plug.GrafanaNetVersion = gplug.Version
plugVersion, err1 := version.NewVersion(plug.Info.Version)
gplugVersion, err2 := version.NewVersion(gplug.Version)
if err1 != nil || err2 != nil {
plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion
} else {
plug.GrafanaNetHasUpdate = plugVersion.LessThan(gplugVersion)
}
}
}
}
resp2, err := httpClient.Get("https://raw.githubusercontent.com/grafana/grafana/master/latest.json")
if err != nil {
log.Tracef("Failed to get latest.json repo from github.com: %v", 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.Tracef("Update check failed, reading response from github.com, %v", err.Error())
return
}
var githubLatest GithubLatest
err = json.Unmarshal(body, &githubLatest)
if err != nil {
log.Tracef("Failed to unmarshal github.com latest, reading response from github.com: %v", err.Error())
return
}
if strings.Contains(setting.BuildVersion, "-") {
pm.GrafanaLatestVersion = githubLatest.Testing
pm.GrafanaHasUpdate = !strings.HasPrefix(setting.BuildVersion, githubLatest.Testing)
} else {
pm.GrafanaLatestVersion = githubLatest.Stable
pm.GrafanaHasUpdate = githubLatest.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)
}
}