package loader import ( "context" "encoding/json" "errors" "fmt" "os" "path" "path/filepath" "runtime" "strings" "github.com/grafana/grafana/pkg/infra/fs" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "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/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/plugins/storage" "github.com/grafana/grafana/pkg/services/org" "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 { pluginFinder finder.Finder processManager process.Service pluginRegistry registry.Service roleRegistry plugins.RoleRegistry pluginInitializer initializer.Initializer signatureValidator signature.Validator pluginStorage storage.Manager pluginsCDN *pluginscdn.Service assetPath *assetpath.Service log log.Logger cfg *config.Cfg errs map[string]*plugins.SignatureError } func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, roleRegistry plugins.RoleRegistry, pluginsCDNService *pluginscdn.Service, assetPath *assetpath.Service) *Loader { return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry), storage.FileSystem(log.NewPrettyLogger("loader.fs"), cfg.PluginsPath), roleRegistry, pluginsCDNService, assetPath) } func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, processManager process.Service, pluginStorage storage.Manager, roleRegistry plugins.RoleRegistry, pluginsCDNService *pluginscdn.Service, assetPath *assetpath.Service) *Loader { return &Loader{ pluginFinder: finder.New(), pluginRegistry: pluginRegistry, pluginInitializer: initializer.New(cfg, backendProvider, license), signatureValidator: signature.NewValidator(authorizer), processManager: processManager, pluginStorage: pluginStorage, errs: make(map[string]*plugins.SignatureError), log: log.New("plugin.loader"), roleRegistry: roleRegistry, cfg: cfg, pluginsCDN: pluginsCDNService, assetPath: assetPath, } } func (l *Loader) Load(ctx context.Context, class plugins.Class, paths []string) ([]*plugins.Plugin, error) { pluginJSONPaths, err := l.pluginFinder.Find(paths) if err != nil { return nil, err } return l.loadPlugins(ctx, class, pluginJSONPaths) } func (l *Loader) createPluginsForLoading(class plugins.Class, foundPlugins foundPlugins) map[string]*plugins.Plugin { loadedPlugins := make(map[string]*plugins.Plugin) for pluginDir, pluginJSON := range foundPlugins { plugin, err := l.createPluginBase(pluginJSON, class, pluginDir) if err != nil { l.log.Warn("Could not create plugin base", "pluginID", pluginJSON.ID, "err", err) continue } // calculate initial signature state var sig plugins.Signature if l.pluginsCDN.PluginSupported(plugin.ID) { // CDN plugins have no signature checks for now. sig = plugins.Signature{Status: plugins.SignatureValid} } else { 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 loadedPlugins[plugin.PluginDir] = plugin } return loadedPlugins } func (l *Loader) loadPlugins(ctx context.Context, class plugins.Class, pluginJSONPaths []string) ([]*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 } // get all registered plugins registeredPlugins := make(map[string]struct{}) for _, p := range l.pluginRegistry.Plugins(ctx) { registeredPlugins[p.ID] = struct{}{} } foundPlugins.stripDuplicates(registeredPlugins, l.log) // create plugins structs and calculate signatures loadedPlugins := l.createPluginsForLoading(class, foundPlugins) // 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. // CDN plugins can be loaded with plugin.json only, so do not warn for those. 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.pluginsCDN.PluginSupported(plugin.ID) { 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() { configureAppChildPlugin(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)) if errDeclareRoles := l.roleRegistry.DeclarePluginRoles(ctx, p.ID, p.Name, p.Roles); errDeclareRoles != nil { l.log.Warn("Declare plugin roles failed.", "pluginID", p.ID, "path", p.PluginDir, "error", errDeclareRoles) } } for _, p := range verifiedPlugins { if err := l.load(ctx, p); err != nil { l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err) } } return verifiedPlugins, nil } func (l *Loader) Unload(ctx context.Context, pluginID string) error { plugin, exists := l.pluginRegistry.Plugin(ctx, pluginID) if !exists { return plugins.ErrPluginNotInstalled } if !plugin.IsExternalPlugin() { return plugins.ErrUninstallCorePlugin } if err := l.unload(ctx, plugin); err != nil { return err } return nil } func (l *Loader) load(ctx context.Context, p *plugins.Plugin) error { if err := l.pluginRegistry.Add(ctx, p); err != nil { return err } if !p.IsCorePlugin() { l.log.Info("Plugin registered", "pluginID", p.ID) } if p.IsExternalPlugin() { if err := l.pluginStorage.Register(ctx, p.ID, p.PluginDir); err != nil { return err } } return l.processManager.Start(ctx, p.ID) } func (l *Loader) unload(ctx context.Context, p *plugins.Plugin) error { l.log.Debug("Stopping plugin process", "pluginId", p.ID) // TODO confirm the sequence of events is safe if err := l.processManager.Stop(ctx, p.ID); err != nil { return err } if err := l.pluginRegistry.Remove(ctx, p.ID); err != nil { return err } l.log.Debug("Plugin unregistered", "pluginId", p.ID) if err := l.pluginStorage.Remove(ctx, p.ID); err != nil { return err } return 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 = org.RoleViewer } } for i, extension := range plugin.Extensions { if !filepath.IsAbs(extension.Path) { plugin.Extensions[i].Path = path.Join("/", extension.Path) } } return plugin, nil } func (l *Loader) createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (*plugins.Plugin, error) { baseURL, err := l.assetPath.Base(pluginJSON, class, pluginDir) if err != nil { return nil, fmt.Errorf("base url: %w", err) } moduleURL, err := l.assetPath.Module(pluginJSON, class, pluginDir) if err != nil { return nil, fmt.Errorf("module url: %w", err) } plugin := &plugins.Plugin{ JSONData: pluginJSON, PluginDir: pluginDir, BaseURL: baseURL, Module: moduleURL, Class: class, } plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID))) if err := l.setImages(plugin); err != nil { return nil, err } return plugin, nil } func (l *Loader) setImages(p *plugins.Plugin) error { var err error for _, dst := range []*string{&p.Info.Logos.Small, &p.Info.Logos.Large} { *dst, err = l.assetPath.RelativeURL(p, *dst, defaultLogoPath(p.Type)) if err != nil { return fmt.Errorf("logo: %w", err) } } for i := 0; i < len(p.Info.Screenshots); i++ { screenshot := &p.Info.Screenshots[i] screenshot.Path, err = l.assetPath.RelativeURL(p, screenshot.Path, "") if err != nil { return fmt.Errorf("screenshot %d relative url: %w", i, err) } } return nil } func setDefaultNavURL(p *plugins.Plugin) { // slugify pages for _, include := range p.Includes { if include.Slug == "" { include.Slug = slugify.Slugify(include.Name) } if !include.DefaultNav { continue } if include.Type == "page" { p.DefaultNavURL = path.Join("/plugins/", p.ID, "/page/", include.Slug) } if include.Type == "dashboard" { dboardURL := include.DashboardURLPath() if dboardURL == "" { p.Logger().Warn("Included dashboard is missing a UID field") continue } p.DefaultNavURL = dboardURL } } } func configureAppChildPlugin(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 defaultLogoPath(pluginType plugins.Type) string { return "public/img/icn-" + string(pluginType) + ".svg" } 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 } 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{}{} } }