Plugins: Enable plugin runtime install/uninstall capabilities (#33836)

* add uninstall flow

* add install flow

* small cleanup

* smaller-footprint solution

* cleanup + make bp start auto

* fix interface contract

* improve naming

* accept version arg

* ensure use of shared logger

* make installer a field

* add plugin decommissioning

* add basic error checking

* fix api docs

* making initialization idempotent

* add mutex

* fix comment

* fix test

* add test for decommission

* improve existing test

* add more test coverage

* more tests

* change test func to use read lock

* refactoring + adding test asserts

* improve purging old install flow

* improve dupe checking

* change log name

* skip over dupe scanned

* make test assertion more flexible

* remove trailing line

* fix pointer receiver name

* update comment

* add context to API

* add config flag

* add base http api test + fix update functionality

* simplify existing check

* clean up test

* refactor tests based on feedback

* add single quotes to errs

* use gcmp in tests + fix logo issue

* make plugin list testing more flexible

* address feedback

* fix API test

* fix linter

* undo preallocate

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* fix linting issue in test

* add docs placeholder

* update install notes

* Update docs/sources/plugins/marketplace.md

Co-authored-by: Marcus Olsson <marcus.olsson@hey.com>

* update access wording

* add more placeholder docs

* add link to more info

* PR feedback - improved errors, refactor, lock fix

* improve err details

* propagate plugin version errors

* don't autostart renderer

* add H1

* fix imports

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
Co-authored-by: Marcus Olsson <marcus.olsson@hey.com>
This commit is contained in:
Will Browne
2021-05-12 20:05:16 +02:00
committed by GitHub
co-authored by achatterjee-grafana Marcus Olsson
parent 1fbadab600
commit c39d6ad97d
35 changed files with 1020 additions and 126 deletions
+199 -20
View File
@@ -12,6 +12,7 @@ import (
"reflect"
"runtime"
"strings"
"sync"
"time"
"github.com/grafana/grafana/pkg/infra/fs"
@@ -20,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"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/registry"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
@@ -28,7 +30,12 @@ import (
)
var (
plog log.Logger
plog log.Logger
installerLog = NewInstallerLogger("plugin.installer", true)
)
const (
grafanaComURL = "https://grafana.com/api/plugins"
)
type unsignedPluginConditionFunc = func(plugin *plugins.PluginBase) bool
@@ -48,6 +55,7 @@ type PluginManager struct {
BackendPluginManager backendplugin.Manager `inject:""`
Cfg *setting.Cfg `inject:""`
SQLStore *sqlstore.SQLStore `inject:""`
pluginInstaller plugins.PluginInstaller
log log.Logger
scanningErrors []error
@@ -64,6 +72,7 @@ type PluginManager struct {
panels map[string]*plugins.PanelPlugin
apps map[string]*plugins.AppPlugin
staticRoutes []*plugins.PluginStaticRoute
pluginsMu sync.RWMutex
}
func init() {
@@ -88,6 +97,7 @@ func (pm *PluginManager) Init() error {
pm.log = log.New("plugins")
plog = log.New("plugins")
pm.pluginScanningErrors = map[string]plugins.PluginError{}
pm.pluginInstaller = installer.New(false, pm.Cfg.BuildVersion, installerLog)
pm.log.Info("Starting plugin search")
@@ -109,11 +119,21 @@ func (pm *PluginManager) Init() error {
}
}
// check if plugins dir exists
exists, err = fs.Exists(pm.Cfg.PluginsPath)
err = pm.initExternalPlugins()
if err != nil {
return err
}
return nil
}
func (pm *PluginManager) initExternalPlugins() error {
// 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)
@@ -132,27 +152,29 @@ func (pm *PluginManager) Init() error {
return err
}
for _, panel := range pm.panels {
var staticRoutesList []*plugins.PluginStaticRoute
for _, panel := range pm.Panels() {
staticRoutes := panel.InitFrontendPlugin(pm.Cfg)
pm.staticRoutes = append(pm.staticRoutes, staticRoutes...)
staticRoutesList = append(staticRoutesList, staticRoutes...)
}
for _, ds := range pm.dataSources {
for _, ds := range pm.DataSources() {
staticRoutes := ds.InitFrontendPlugin(pm.Cfg)
pm.staticRoutes = append(pm.staticRoutes, staticRoutes...)
staticRoutesList = append(staticRoutesList, staticRoutes...)
}
for _, app := range pm.apps {
for _, app := range pm.Apps() {
staticRoutes := app.InitApp(pm.panels, pm.dataSources, pm.Cfg)
pm.staticRoutes = append(pm.staticRoutes, staticRoutes...)
staticRoutesList = append(staticRoutesList, staticRoutes...)
}
if pm.renderer != nil {
if pm.Renderer() != nil {
staticRoutes := pm.renderer.InitFrontendPlugin(pm.Cfg)
pm.staticRoutes = append(pm.staticRoutes, staticRoutes...)
staticRoutesList = append(staticRoutesList, staticRoutes...)
}
pm.staticRoutes = staticRoutesList
for _, p := range pm.plugins {
for _, p := range pm.Plugins() {
if p.IsCorePlugin {
p.Signature = plugins.PluginSignatureInternal
} else {
@@ -182,14 +204,23 @@ func (pm *PluginManager) Run(ctx context.Context) error {
}
func (pm *PluginManager) Renderer() *plugins.RendererPlugin {
pm.pluginsMu.RLock()
defer pm.pluginsMu.RUnlock()
return pm.renderer
}
func (pm *PluginManager) GetDataSource(id string) *plugins.DataSourcePlugin {
pm.pluginsMu.RLock()
defer pm.pluginsMu.RUnlock()
return pm.dataSources[id]
}
func (pm *PluginManager) DataSources() []*plugins.DataSourcePlugin {
pm.pluginsMu.RLock()
defer pm.pluginsMu.RUnlock()
var rslt []*plugins.DataSourcePlugin
for _, ds := range pm.dataSources {
rslt = append(rslt, ds)
@@ -199,18 +230,30 @@ func (pm *PluginManager) DataSources() []*plugins.DataSourcePlugin {
}
func (pm *PluginManager) DataSourceCount() int {
pm.pluginsMu.RLock()
defer pm.pluginsMu.RUnlock()
return len(pm.dataSources)
}
func (pm *PluginManager) PanelCount() int {
pm.pluginsMu.RLock()
defer pm.pluginsMu.RUnlock()
return len(pm.panels)
}
func (pm *PluginManager) AppCount() int {
pm.pluginsMu.RLock()
defer pm.pluginsMu.RUnlock()
return len(pm.apps)
}
func (pm *PluginManager) Plugins() []*plugins.PluginBase {
pm.pluginsMu.RLock()
defer pm.pluginsMu.RUnlock()
var rslt []*plugins.PluginBase
for _, p := range pm.plugins {
rslt = append(rslt, p)
@@ -220,6 +263,9 @@ func (pm *PluginManager) Plugins() []*plugins.PluginBase {
}
func (pm *PluginManager) Apps() []*plugins.AppPlugin {
pm.pluginsMu.RLock()
defer pm.pluginsMu.RUnlock()
var rslt []*plugins.AppPlugin
for _, p := range pm.apps {
rslt = append(rslt, p)
@@ -228,11 +274,29 @@ func (pm *PluginManager) Apps() []*plugins.AppPlugin {
return rslt
}
func (pm *PluginManager) Panels() []*plugins.PanelPlugin {
pm.pluginsMu.RLock()
defer pm.pluginsMu.RUnlock()
var rslt []*plugins.PanelPlugin
for _, p := range pm.panels {
rslt = append(rslt, p)
}
return rslt
}
func (pm *PluginManager) GetPlugin(id string) *plugins.PluginBase {
pm.pluginsMu.RLock()
defer pm.pluginsMu.RUnlock()
return pm.plugins[id]
}
func (pm *PluginManager) GetApp(id string) *plugins.AppPlugin {
pm.pluginsMu.RLock()
defer pm.pluginsMu.RUnlock()
return pm.apps[id]
}
@@ -290,6 +354,25 @@ func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error {
pm.log.Debug("Initial plugin loading done")
pluginsByID := make(map[string]struct{})
for scannedPluginPath, scannedPlugin := range scanner.plugins {
// Check if scanning found duplicate plugins
if _, dupe := pluginsByID[scannedPlugin.Id]; dupe {
pm.log.Warn("Skipping plugin as it's a duplicate", "id", scannedPlugin.Id)
scanner.errors = append(scanner.errors,
plugins.DuplicatePluginError{PluginID: scannedPlugin.Id, ExistingPluginDir: scannedPlugin.PluginDir})
delete(scanner.plugins, scannedPluginPath)
continue
}
pluginsByID[scannedPlugin.Id] = struct{}{}
// Check if scanning found plugins that are already installed
if existing := pm.GetPlugin(scannedPlugin.Id); existing != nil {
pm.log.Debug("Skipping plugin as it's already installed", "plugin", existing.Id, "version", existing.Info.Version)
delete(scanner.plugins, scannedPluginPath)
}
}
pluginTypes := map[string]interface{}{
"panel": plugins.PanelPlugin{},
"datasource": plugins.DataSourcePlugin{},
@@ -371,7 +454,7 @@ func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error {
}
if len(scanner.errors) > 0 {
pm.log.Warn("Some plugins failed to load", "errors", scanner.errors)
pm.log.Warn("Some plugin scanning errors were found", "errors", scanner.errors)
pm.scanningErrors = scanner.errors
}
@@ -385,6 +468,9 @@ func (pm *PluginManager) loadPlugin(jsonParser *json.Decoder, pluginBase *plugin
return err
}
pm.pluginsMu.Lock()
defer pm.pluginsMu.Unlock()
var pb *plugins.PluginBase
switch p := plug.(type) {
case *plugins.DataSourcePlugin:
@@ -403,12 +489,6 @@ func (pm *PluginManager) loadPlugin(jsonParser *json.Decoder, pluginBase *plugin
panic(fmt.Sprintf("Unrecognized plugin type %T", plug))
}
if p, exists := pm.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)
}
@@ -666,7 +746,10 @@ func collectPluginFilesWithin(rootDir string) ([]string, error) {
// GetDataPlugin gets a DataPlugin with a certain name. If none is found, nil is returned.
//nolint: staticcheck // plugins.DataPlugin deprecated
func (pm *PluginManager) GetDataPlugin(id string) plugins.DataPlugin {
if p, exists := pm.dataSources[id]; exists && p.CanHandleDataQueries() {
pm.pluginsMu.RLock()
defer pm.pluginsMu.RUnlock()
if p := pm.GetDataSource(id); p != nil && p.CanHandleDataQueries() {
return p
}
@@ -683,3 +766,99 @@ func (pm *PluginManager) GetDataPlugin(id string) plugins.DataPlugin {
func (pm *PluginManager) StaticRoutes() []*plugins.PluginStaticRoute {
return pm.staticRoutes
}
func (pm *PluginManager) Install(ctx context.Context, pluginID, version string) error {
plugin := pm.GetPlugin(pluginID)
if plugin != nil {
if plugin.IsCorePlugin {
return plugins.ErrInstallCorePlugin
}
if plugin.Info.Version == version {
return plugins.DuplicatePluginError{
PluginID: pluginID,
ExistingPluginDir: plugin.PluginDir,
}
}
// remove existing installation of plugin
err := pm.Uninstall(context.Background(), plugin.Id)
if err != nil {
return err
}
}
err := pm.pluginInstaller.Install(ctx, pluginID, version, pm.Cfg.PluginsPath, "", grafanaComURL)
if err != nil {
return err
}
err = pm.initExternalPlugins()
if err != nil {
return err
}
return nil
}
func (pm *PluginManager) Uninstall(ctx context.Context, pluginID string) error {
plugin := pm.GetPlugin(pluginID)
if plugin == nil {
return plugins.ErrPluginNotInstalled
}
if plugin.IsCorePlugin {
return plugins.ErrUninstallCorePlugin
}
// extra security check to ensure we only remove plugins that are located in the configured plugins directory
path, err := filepath.Rel(pm.Cfg.PluginsPath, plugin.PluginDir)
if err != nil || strings.HasPrefix(path, ".."+string(filepath.Separator)) {
return plugins.ErrUninstallOutsideOfPluginDir
}
if pm.BackendPluginManager.IsRegistered(pluginID) {
err := pm.BackendPluginManager.UnregisterAndStop(ctx, pluginID)
if err != nil {
return err
}
}
err = pm.unregister(plugin)
if err != nil {
return err
}
return pm.pluginInstaller.Uninstall(ctx, pluginID, pm.Cfg.PluginsPath)
}
func (pm *PluginManager) unregister(plugin *plugins.PluginBase) error {
pm.pluginsMu.Lock()
defer pm.pluginsMu.Unlock()
switch plugin.Type {
case "panel":
delete(pm.panels, plugin.Id)
case "datasource":
delete(pm.dataSources, plugin.Id)
case "app":
delete(pm.apps, plugin.Id)
case "renderer":
pm.renderer = nil
}
delete(pm.plugins, plugin.Id)
pm.removeStaticRoute(plugin.Id)
return nil
}
func (pm *PluginManager) removeStaticRoute(pluginID string) {
for i, route := range pm.staticRoutes {
if pluginID == route.PluginId {
pm.staticRoutes = append(pm.staticRoutes[:i], pm.staticRoutes[i+1:]...)
return
}
}
}