grafana/pkg/plugins/plugins.go
2020-05-12 15:48:24 +02:00

376 lines
10 KiB
Go

package plugins
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"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/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"
"golang.org/x/xerrors"
)
var (
DataSources map[string]*DataSourcePlugin
Panels map[string]*PanelPlugin
StaticRoutes []*PluginStaticRoute
Apps map[string]*AppPlugin
Plugins map[string]*PluginBase
PluginTypes map[string]interface{}
Renderer *RendererPlugin
Transform *TransformPlugin
GrafanaLatestVersion string
GrafanaHasUpdate bool
plog log.Logger
)
type PluginScanner struct {
pluginPath string
errors []error
backendPluginManager backendplugin.Manager
cfg *setting.Cfg
requireSigned bool
log log.Logger
}
type PluginManager struct {
BackendPluginManager backendplugin.Manager `inject:""`
Cfg *setting.Cfg `inject:""`
log log.Logger
scanningErrors []error
}
func init() {
registry.RegisterService(&PluginManager{})
}
func (pm *PluginManager) Init() error {
pm.log = log.New("plugins")
plog = log.New("plugins")
DataSources = map[string]*DataSourcePlugin{}
StaticRoutes = []*PluginStaticRoute{}
Panels = map[string]*PanelPlugin{}
Apps = map[string]*AppPlugin{}
Plugins = map[string]*PluginBase{}
PluginTypes = map[string]interface{}{
"panel": PanelPlugin{},
"datasource": DataSourcePlugin{},
"app": AppPlugin{},
"renderer": RendererPlugin{},
"transform": TransformPlugin{},
}
pm.log.Info("Starting plugin search")
plugDir := path.Join(setting.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(setting.PluginsPath)
if err != nil {
return err
}
if !exists {
if err = os.MkdirAll(setting.PluginsPath, os.ModePerm); err != nil {
pm.log.Error("failed to create external plugins directory", "dir", setting.PluginsPath, "error", err)
} else {
pm.log.Info("External plugins directory created", "directory", setting.PluginsPath)
}
} else {
pm.log.Debug("Scanning external plugins directory", "dir", setting.PluginsPath)
if err := pm.scan(setting.PluginsPath, true); err != nil {
return errutil.Wrapf(err, "failed to scan external plugins directory '%s'",
setting.PluginsPath)
}
}
// check plugin paths defined in config
if err := pm.checkPluginPaths(); err != nil {
return err
}
for _, panel := range Panels {
panel.initFrontendPlugin()
}
for _, ds := range DataSources {
ds.initFrontendPlugin()
}
for _, app := range Apps {
app.initApp()
}
if Renderer != nil {
Renderer.initFrontendPlugin()
}
for _, p := range Plugins {
if p.IsCorePlugin {
p.Signature = PluginSignatureInternal
} else {
p.Signature = getPluginSignatureState(pm.log, p)
metrics.SetPluginBuildInformation(p.Id, p.Type, p.Info.Version)
}
}
return nil
}
func (pm *PluginManager) Run(ctx context.Context) error {
pm.updateAppDashboards()
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()
}
func (pm *PluginManager) checkPluginPaths() 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,
}
if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil {
if xerrors.Is(err, os.ErrNotExist) {
pm.log.Debug("Couldn't scan directory since it doesn't exist", "pluginDir", pluginDir)
return nil
}
if xerrors.Is(err, os.ErrPermission) {
pm.log.Debug("Couldn't scan directory due to lack of permissions", "pluginDir", pluginDir)
return nil
}
if pluginDir != "data/plugins" {
pm.log.Warn("Could not scan dir", "pluginDir", pluginDir, "err", err)
}
return err
}
if len(scanner.errors) > 0 {
pm.log.Warn("Some plugins failed to load", "errors", scanner.errors)
pm.scanningErrors = scanner.errors
}
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) (*DataSourcePlugin, bool) {
ds, exist := DataSources[pluginID]
return ds, exist
}
func (scanner *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 err
}
if f.Name() == "node_modules" || f.Name() == "Chromium.app" {
return util.ErrWalkSkipDir
}
if f.IsDir() {
return nil
}
if f.Name() == "plugin.json" {
err := scanner.loadPlugin(currentPath)
if err != nil {
scanner.log.Error("Failed to load plugin", "error", err, "pluginPath", filepath.Dir(currentPath))
scanner.errors = append(scanner.errors, err)
}
}
return nil
}
func (scanner *PluginScanner) loadPlugin(pluginJsonFilePath string) error {
currentDir := filepath.Dir(pluginJsonFilePath)
reader, err := os.Open(pluginJsonFilePath)
if err != nil {
return err
}
defer reader.Close()
jsonParser := json.NewDecoder(reader)
pluginCommon := 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")
}
// The expressions feature toggle corresponds to transform plug-ins.
if pluginCommon.Type == "transform" {
isEnabled := scanner.cfg.IsExpressionsEnabled()
if !isEnabled {
scanner.log.Debug("Transform plugin is disabled since the expressions feature toggle is not enabled",
"pluginID", pluginCommon.Id)
return nil
}
}
pluginCommon.PluginDir = filepath.Dir(pluginJsonFilePath)
// 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 pluginCommon.Backend && scanner.requireSigned {
sig := getPluginSignatureState(scanner.log, &pluginCommon)
if sig != PluginSignatureValid {
scanner.log.Debug("Invalid Plugin Signature", "pluginID", pluginCommon.Id, "pluginDir", pluginCommon.PluginDir, "state", sig)
if sig == PluginSignatureUnsigned {
allowUnsigned := false
for _, plug := range scanner.cfg.PluginsAllowUnsigned {
if plug == pluginCommon.Id {
allowUnsigned = true
break
}
}
if setting.Env != setting.DEV && !allowUnsigned {
return fmt.Errorf("plugin %q is unsigned", pluginCommon.Id)
}
scanner.log.Warn("Running an unsigned backend plugin", "pluginID", pluginCommon.Id, "pluginDir", pluginCommon.PluginDir)
} else {
switch sig {
case PluginSignatureInvalid:
return fmt.Errorf("plugin %q has an invalid signature", pluginCommon.Id)
case PluginSignatureModified:
return fmt.Errorf("plugin %q's signature has been modified", pluginCommon.Id)
default:
return fmt.Errorf("unrecognized plugin signature state %v", sig)
}
}
}
}
pluginGoType, exists := PluginTypes[pluginCommon.Type]
if !exists {
return fmt.Errorf("unknown plugin type %q", pluginCommon.Type)
}
loader := reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader)
// External plugins need a module.js file for SystemJS to load
if !strings.HasPrefix(pluginJsonFilePath, setting.StaticRootPath) && !scanner.IsBackendOnlyPlugin(pluginCommon.Type) {
module := filepath.Join(filepath.Dir(pluginJsonFilePath), "module.js")
exists, err := fs.Exists(module)
if err != nil {
return err
}
if !exists {
scanner.log.Warn("Plugin missing module.js",
"name", pluginCommon.Name,
"warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.",
"path", module)
}
}
if _, err := reader.Seek(0, 0); err != nil {
return err
}
return loader.Load(jsonParser, currentDir, scanner.backendPluginManager)
}
func (scanner *PluginScanner) IsBackendOnlyPlugin(pluginType string) bool {
return pluginType == "renderer" || pluginType == "transform"
}
func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
plug, exists := Plugins[pluginId]
if !exists {
return nil, PluginNotFoundError{pluginId}
}
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
}
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return data, nil
}