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:
co-authored by
achatterjee-grafana
Marcus Olsson
parent
1fbadab600
commit
c39d6ad97d
+199
-20
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user