grafana/pkg/plugins/manager/manager.go
Will Browne 23dde457ef
Plugins: Don't start runner ticker for Core plugins (#46964)
* Plugins: Don't start runner ticker for Core plugins

* add test

* correct start check in test
2022-03-30 14:48:49 +02:00

283 lines
6.8 KiB
Go

package manager
import (
"context"
"errors"
"fmt"
"path/filepath"
"sync"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/manager/installer"
"github.com/grafana/grafana/pkg/setting"
)
const (
grafanaComURL = "https://grafana.com/api/plugins"
)
var _ plugins.Client = (*PluginManager)(nil)
var _ plugins.Store = (*PluginManager)(nil)
var _ plugins.StaticRouteResolver = (*PluginManager)(nil)
var _ plugins.RendererManager = (*PluginManager)(nil)
type PluginManager struct {
cfg *plugins.Cfg
store map[string]*plugins.Plugin
pluginInstaller plugins.Installer
pluginLoader plugins.Loader
pluginsMu sync.RWMutex
pluginSources []PluginSource
log log.Logger
}
type PluginSource struct {
Class plugins.Class
Paths []string
}
func ProvideService(grafanaCfg *setting.Cfg, pluginLoader plugins.Loader) (*PluginManager, error) {
pm := New(plugins.FromGrafanaCfg(grafanaCfg), []PluginSource{
{Class: plugins.Core, Paths: corePluginPaths(grafanaCfg)},
{Class: plugins.Bundled, Paths: []string{grafanaCfg.BundledPluginsPath}},
{Class: plugins.External, Paths: append([]string{grafanaCfg.PluginsPath}, pluginSettingPaths(grafanaCfg)...)},
}, pluginLoader)
if err := pm.Init(); err != nil {
return nil, err
}
return pm, nil
}
func New(cfg *plugins.Cfg, pluginSources []PluginSource, pluginLoader plugins.Loader) *PluginManager {
return &PluginManager{
cfg: cfg,
pluginLoader: pluginLoader,
pluginSources: pluginSources,
store: make(map[string]*plugins.Plugin),
log: log.New("plugin.manager"),
pluginInstaller: installer.New(false, cfg.BuildVersion, newInstallerLogger("plugin.installer", true)),
}
}
func (m *PluginManager) Init() error {
for _, ps := range m.pluginSources {
err := m.loadPlugins(context.Background(), ps.Class, ps.Paths...)
if err != nil {
return err
}
}
return nil
}
func (m *PluginManager) Run(ctx context.Context) error {
<-ctx.Done()
m.shutdown(ctx)
return ctx.Err()
}
func (m *PluginManager) loadPlugins(ctx context.Context, class plugins.Class, paths ...string) error {
if len(paths) == 0 {
return nil
}
var pluginPaths []string
for _, p := range paths {
if p != "" {
pluginPaths = append(pluginPaths, p)
}
}
loadedPlugins, err := m.pluginLoader.Load(ctx, class, pluginPaths, m.registeredPlugins())
if err != nil {
m.log.Error("Could not load plugins", "paths", pluginPaths, "err", err)
return err
}
for _, p := range loadedPlugins {
if err := m.registerAndStart(context.Background(), p); err != nil {
m.log.Error("Could not start plugin", "pluginId", p.ID, "err", err)
}
}
return nil
}
func (m *PluginManager) Renderer() *plugins.Plugin {
for _, p := range m.plugins() {
if p.IsRenderer() {
return p
}
}
return nil
}
func (m *PluginManager) Routes() []*plugins.StaticRoute {
staticRoutes := make([]*plugins.StaticRoute, 0)
for _, p := range m.plugins() {
if p.StaticRoute() != nil {
staticRoutes = append(staticRoutes, p.StaticRoute())
}
}
return staticRoutes
}
func (m *PluginManager) registerAndStart(ctx context.Context, plugin *plugins.Plugin) error {
err := m.register(plugin)
if err != nil {
return err
}
if !m.isRegistered(plugin.ID) {
return fmt.Errorf("plugin %s is not registered", plugin.ID)
}
return m.start(ctx, plugin)
}
func (m *PluginManager) register(p *plugins.Plugin) error {
if m.isRegistered(p.ID) {
return fmt.Errorf("plugin %s is already registered", p.ID)
}
m.pluginsMu.Lock()
m.store[p.ID] = p
m.pluginsMu.Unlock()
if !p.IsCorePlugin() {
m.log.Info("Plugin registered", "pluginId", p.ID)
}
return nil
}
func (m *PluginManager) unregisterAndStop(ctx context.Context, p *plugins.Plugin) error {
m.log.Debug("Stopping plugin process", "pluginId", p.ID)
m.pluginsMu.Lock()
defer m.pluginsMu.Unlock()
if err := p.Decommission(); err != nil {
return err
}
if err := p.Stop(ctx); err != nil {
return err
}
delete(m.store, p.ID)
m.log.Debug("Plugin unregistered", "pluginId", p.ID)
return nil
}
// start starts a backend plugin process
func (m *PluginManager) start(ctx context.Context, p *plugins.Plugin) error {
if !p.IsManaged() || !p.Backend || p.SignatureError != nil {
return nil
}
if !m.isRegistered(p.ID) {
return backendplugin.ErrPluginNotRegistered
}
if p.IsCorePlugin() {
return nil
}
if err := startPluginAndRestartKilledProcesses(ctx, p); err != nil {
return err
}
p.Logger().Debug("Successfully started backend plugin process")
return nil
}
func startPluginAndRestartKilledProcesses(ctx context.Context, p *plugins.Plugin) error {
if err := p.Start(ctx); err != nil {
return err
}
go func(ctx context.Context, p *plugins.Plugin) {
if err := restartKilledProcess(ctx, p); err != nil {
p.Logger().Error("Attempt to restart killed plugin process failed", "error", err)
}
}(ctx, p)
return nil
}
func restartKilledProcess(ctx context.Context, p *plugins.Plugin) error {
ticker := time.NewTicker(time.Second * 1)
for {
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) {
return err
}
return nil
case <-ticker.C:
if p.IsDecommissioned() {
p.Logger().Debug("Plugin decommissioned")
return nil
}
if !p.Exited() {
continue
}
p.Logger().Debug("Restarting plugin")
if err := p.Start(ctx); err != nil {
p.Logger().Error("Failed to restart plugin", "error", err)
continue
}
p.Logger().Debug("Plugin restarted")
}
}
}
// shutdown stops all backend plugin processes
func (m *PluginManager) shutdown(ctx context.Context) {
var wg sync.WaitGroup
for _, p := range m.plugins() {
wg.Add(1)
go func(p backendplugin.Plugin, ctx context.Context) {
defer wg.Done()
p.Logger().Debug("Stopping plugin")
if err := p.Stop(ctx); err != nil {
p.Logger().Error("Failed to stop plugin", "error", err)
}
p.Logger().Debug("Plugin stopped")
}(p, ctx)
}
wg.Wait()
}
// corePluginPaths provides a list of the Core plugin paths which need to be scanned on init()
func corePluginPaths(cfg *setting.Cfg) []string {
datasourcePaths := filepath.Join(cfg.StaticRootPath, "app/plugins/datasource")
panelsPath := filepath.Join(cfg.StaticRootPath, "app/plugins/panel")
return []string{datasourcePaths, panelsPath}
}
// pluginSettingPaths provides a plugin paths defined in cfg.PluginSettings which need to be scanned on init()
func pluginSettingPaths(cfg *setting.Cfg) []string {
var pluginSettingDirs []string
for _, settings := range cfg.PluginSettings {
path, exists := settings["path"]
if !exists || path == "" {
continue
}
pluginSettingDirs = append(pluginSettingDirs, path)
}
return pluginSettingDirs
}