mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			374 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			374 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
| package loader
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"path/filepath"
 | |
| 	"runtime"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/gosimple/slug"
 | |
| 
 | |
| 	"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/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"
 | |
| 	"github.com/grafana/grafana/pkg/util"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	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                *plugins.Cfg
 | |
| 	pluginFinder       finder.Finder
 | |
| 	pluginInitializer  initializer.Initializer
 | |
| 	signatureValidator signature.Validator
 | |
| 	log                log.Logger
 | |
| 
 | |
| 	errs map[string]*plugins.SignatureError
 | |
| }
 | |
| 
 | |
| func ProvideService(cfg *setting.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer,
 | |
| 	backendProvider plugins.BackendFactoryProvider) (*Loader, error) {
 | |
| 	return New(plugins.FromGrafanaCfg(cfg), license, authorizer, backendProvider), nil
 | |
| }
 | |
| 
 | |
| func New(cfg *plugins.Cfg, license models.Licensing, authorizer plugins.PluginLoaderAuthorizer,
 | |
| 	backendProvider plugins.BackendFactoryProvider) *Loader {
 | |
| 	return &Loader{
 | |
| 		cfg:                cfg,
 | |
| 		pluginFinder:       finder.New(),
 | |
| 		pluginInitializer:  initializer.New(cfg, backendProvider, license),
 | |
| 		signatureValidator: signature.NewValidator(authorizer),
 | |
| 		errs:               make(map[string]*plugins.SignatureError),
 | |
| 		log:                log.New("plugin.loader"),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (l *Loader) Load(ctx context.Context, class plugins.Class, paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) {
 | |
| 	pluginJSONPaths, err := l.pluginFinder.Find(paths)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return l.loadPlugins(ctx, class, pluginJSONPaths, ignore)
 | |
| }
 | |
| 
 | |
| func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, 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 {
 | |
| 			l.log.Warn("Skipping plugin loading as its plugin.json could not be read", "path", pluginJSONPath, "err", err)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		pluginJSONAbsPath, err := filepath.Abs(pluginJSONPath)
 | |
| 		if err != nil {
 | |
| 			l.log.Warn("Skipping plugin loading as absolute plugin.json path could not be calculated", "pluginID", plugin.ID, "err", err)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if _, dupe := foundPlugins[filepath.Dir(pluginJSONAbsPath)]; dupe {
 | |
| 			l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", plugin.ID)
 | |
| 			continue
 | |
| 		}
 | |
| 		foundPlugins[filepath.Dir(pluginJSONAbsPath)] = plugin
 | |
| 	}
 | |
| 
 | |
| 	foundPlugins.stripDuplicates(existingPlugins, l.log)
 | |
| 
 | |
| 	// calculate initial signature state
 | |
| 	loadedPlugins := make(map[string]*plugins.Plugin)
 | |
| 	for pluginDir, pluginJSON := range foundPlugins {
 | |
| 		plugin := createPluginBase(pluginJSON, class, pluginDir, l.log)
 | |
| 
 | |
| 		sig, err := signature.Calculate(l.log, plugin)
 | |
| 		if err != nil {
 | |
| 			l.log.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 := make([]*plugins.Plugin, 0)
 | |
| 	for _, plugin := range loadedPlugins {
 | |
| 		signingError := l.signatureValidator.Validate(plugin)
 | |
| 		if signingError != nil {
 | |
| 			l.log.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 {
 | |
| 				l.log.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)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if plugin.IsApp() {
 | |
| 			setDefaultNavURL(plugin)
 | |
| 		}
 | |
| 
 | |
| 		if plugin.Parent != nil && plugin.Parent.IsApp() {
 | |
| 			configureAppChildOPlugin(plugin.Parent, plugin)
 | |
| 		}
 | |
| 
 | |
| 		verifiedPlugins = append(verifiedPlugins, plugin)
 | |
| 	}
 | |
| 
 | |
| 	for _, p := range verifiedPlugins {
 | |
| 		err := l.pluginInitializer.Initialize(ctx, p)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature))
 | |
| 	}
 | |
| 
 | |
| 	return verifiedPlugins, nil
 | |
| }
 | |
| 
 | |
| func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) {
 | |
| 	l.log.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 {
 | |
| 		l.log.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)"
 | |
| 	}
 | |
| 
 | |
| 	if len(plugin.Dependencies.Plugins) == 0 {
 | |
| 		plugin.Dependencies.Plugins = []plugins.Dependency{}
 | |
| 	}
 | |
| 
 | |
| 	if plugin.Dependencies.GrafanaVersion == "" {
 | |
| 		plugin.Dependencies.GrafanaVersion = "*"
 | |
| 	}
 | |
| 
 | |
| 	for _, include := range plugin.Includes {
 | |
| 		if include.Role == "" {
 | |
| 			include.Role = models.ROLE_VIEWER
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return plugin, nil
 | |
| }
 | |
| 
 | |
| func createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string, logger log.Logger) *plugins.Plugin {
 | |
| 	plugin := &plugins.Plugin{
 | |
| 		JSONData:  pluginJSON,
 | |
| 		PluginDir: pluginDir,
 | |
| 		BaseURL:   baseURL(pluginJSON, class, pluginDir),
 | |
| 		Module:    module(pluginJSON, class, pluginDir),
 | |
| 		Class:     class,
 | |
| 	}
 | |
| 
 | |
| 	plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID)))
 | |
| 	setImages(plugin)
 | |
| 
 | |
| 	return plugin
 | |
| }
 | |
| 
 | |
| func setImages(p *plugins.Plugin) {
 | |
| 	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)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func setDefaultNavURL(p *plugins.Plugin) {
 | |
| 	// slugify pages
 | |
| 	for _, include := range p.Includes {
 | |
| 		if include.Slug == "" {
 | |
| 			include.Slug = slug.Make(include.Name)
 | |
| 		}
 | |
| 
 | |
| 		if !include.DefaultNav {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if include.Type == "page" {
 | |
| 			p.DefaultNavURL = path.Join("/plugins/", p.ID, "/page/", include.Slug)
 | |
| 		}
 | |
| 		if include.Type == "dashboard" {
 | |
| 			p.DefaultNavURL = path.Join("/dashboard/db/", include.Slug)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func configureAppChildOPlugin(parent *plugins.Plugin, child *plugins.Plugin) {
 | |
| 	if !parent.IsApp() {
 | |
| 		return
 | |
| 	}
 | |
| 	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 (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 baseURL(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) string {
 | |
| 	if class == plugins.Core {
 | |
| 		return path.Join("public/app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir))
 | |
| 	}
 | |
| 
 | |
| 	return path.Join("public/plugins", pluginJSON.ID)
 | |
| }
 | |
| 
 | |
| func module(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) string {
 | |
| 	if class == plugins.Core {
 | |
| 		return path.Join("app/plugins", string(pluginJSON.Type), filepath.Base(pluginDir), "module")
 | |
| 	}
 | |
| 
 | |
| 	return path.Join("plugins", pluginJSON.ID, "module")
 | |
| }
 | |
| 
 | |
| func validatePluginJSON(data plugins.JSONData) error {
 | |
| 	if data.ID == "" || !data.Type.IsValid() {
 | |
| 		return ErrInvalidPluginJSON
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| type foundPlugins map[string]plugins.JSONData
 | |
| 
 | |
| // stripDuplicates will strip duplicate plugins or plugins that already exist
 | |
| func (f *foundPlugins) stripDuplicates(existingPlugins map[string]struct{}, log log.Logger) {
 | |
| 	pluginsByID := make(map[string]struct{})
 | |
| 	for k, scannedPlugin := range *f {
 | |
| 		if _, existing := existingPlugins[scannedPlugin.ID]; existing {
 | |
| 			log.Debug("Skipping plugin as it's already installed", "plugin", scannedPlugin.ID)
 | |
| 			delete(*f, k)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		pluginsByID[scannedPlugin.ID] = struct{}{}
 | |
| 	}
 | |
| }
 |