Plugins: Plugin Store API returns DTO model (#41340)

* toying around

* fix refs

* remove unused fields

* go further

* add context

* ensure streaming handler is set
This commit is contained in:
Will Browne
2021-11-17 11:04:22 +00:00
committed by GitHub
parent dbb8246b6b
commit 2e3e7a7e55
24 changed files with 494 additions and 353 deletions

View File

@@ -13,9 +13,9 @@ import (
// Store is the storage for plugins.
type Store interface {
// Plugin finds a plugin by its ID.
Plugin(pluginID string) *Plugin
Plugin(ctx context.Context, pluginID string) (PluginDTO, bool)
// Plugins returns plugins by their requested type.
Plugins(pluginTypes ...Type) []*Plugin
Plugins(ctx context.Context, pluginTypes ...Type) []PluginDTO
// Add adds a plugin to the store.
Add(ctx context.Context, pluginID, version string, opts AddOpts) error

View File

@@ -13,8 +13,8 @@ import (
)
func (m *PluginManager) GetPluginDashboards(orgID int64, pluginID string) ([]*plugins.PluginDashboardInfoDTO, error) {
plugin := m.Plugin(pluginID)
if plugin == nil {
plugin, exists := m.Plugin(context.TODO(), pluginID)
if !exists {
return nil, plugins.NotFoundError{PluginID: pluginID}
}
@@ -73,8 +73,8 @@ func (m *PluginManager) GetPluginDashboards(orgID int64, pluginID string) ([]*pl
}
func (m *PluginManager) LoadPluginDashboard(pluginID, path string) (*models.Dashboard, error) {
plugin := m.Plugin(pluginID)
if plugin == nil {
plugin, exists := m.Plugin(context.TODO(), pluginID)
if !exists {
return nil, plugins.NotFoundError{PluginID: pluginID}
}

View File

@@ -11,7 +11,6 @@ import (
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
@@ -45,7 +44,7 @@ type PluginManager struct {
cfg *setting.Cfg
requestValidator models.PluginRequestValidator
sqlStore *sqlstore.SQLStore
plugins map[string]*plugins.Plugin
store map[string]*plugins.Plugin
pluginInstaller plugins.Installer
pluginLoader plugins.Loader
pluginsMu sync.RWMutex
@@ -68,7 +67,7 @@ func newManager(cfg *setting.Cfg, pluginRequestValidator models.PluginRequestVal
requestValidator: pluginRequestValidator,
sqlStore: sqlStore,
pluginLoader: pluginLoader,
plugins: map[string]*plugins.Plugin{},
store: map[string]*plugins.Plugin{},
log: log.New("plugin.manager"),
pluginInstaller: installer.New(false, cfg.BuildVersion, newInstallerLogger("plugin.installer", true)),
}
@@ -138,10 +137,36 @@ func (m *PluginManager) Run(ctx context.Context) error {
}
<-ctx.Done()
m.stop(ctx)
m.shutdown(ctx)
return ctx.Err()
}
func (m *PluginManager) plugin(pluginID string) (*plugins.Plugin, bool) {
m.pluginsMu.RLock()
defer m.pluginsMu.RUnlock()
p, exists := m.store[pluginID]
if !exists || (p.IsDecommissioned()) {
return nil, false
}
return p, true
}
func (m *PluginManager) plugins() []*plugins.Plugin {
m.pluginsMu.RLock()
defer m.pluginsMu.RUnlock()
res := make([]*plugins.Plugin, 0)
for _, p := range m.store {
if !p.IsDecommissioned() {
res = append(res, p)
}
}
return res
}
func (m *PluginManager) loadPlugins(paths ...string) error {
if len(paths) == 0 {
return nil
@@ -171,52 +196,15 @@ func (m *PluginManager) loadPlugins(paths ...string) error {
func (m *PluginManager) registeredPlugins() map[string]struct{} {
pluginsByID := make(map[string]struct{})
m.pluginsMu.RLock()
defer m.pluginsMu.RUnlock()
for _, p := range m.plugins {
for _, p := range m.plugins() {
pluginsByID[p.ID] = struct{}{}
}
return pluginsByID
}
func (m *PluginManager) Plugin(pluginID string) *plugins.Plugin {
m.pluginsMu.RLock()
p, ok := m.plugins[pluginID]
m.pluginsMu.RUnlock()
if ok && (p.IsDecommissioned()) {
return nil
}
return p
}
func (m *PluginManager) Plugins(pluginTypes ...plugins.Type) []*plugins.Plugin {
// if no types passed, assume all
if len(pluginTypes) == 0 {
pluginTypes = plugins.PluginTypes
}
var requestedTypes = make(map[plugins.Type]struct{})
for _, pt := range pluginTypes {
requestedTypes[pt] = struct{}{}
}
m.pluginsMu.RLock()
var pluginsList []*plugins.Plugin
for _, p := range m.plugins {
if _, exists := requestedTypes[p.Type]; exists {
pluginsList = append(pluginsList, p)
}
}
m.pluginsMu.RUnlock()
return pluginsList
}
func (m *PluginManager) Renderer() *plugins.Plugin {
for _, p := range m.plugins {
for _, p := range m.plugins() {
if p.IsRenderer() {
return p
}
@@ -226,8 +214,8 @@ func (m *PluginManager) Renderer() *plugins.Plugin {
}
func (m *PluginManager) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
plugin := m.Plugin(req.PluginContext.PluginID)
if plugin == nil {
plugin, exists := m.plugin(req.PluginContext.PluginID)
if !exists {
return nil, backendplugin.ErrPluginNotRegistered
}
@@ -291,8 +279,8 @@ func (m *PluginManager) CallResource(pCtx backend.PluginContext, reqCtx *models.
}
func (m *PluginManager) callResourceInternal(w http.ResponseWriter, req *http.Request, pCtx backend.PluginContext) error {
p := m.Plugin(pCtx.PluginID)
if p == nil {
p, exists := m.plugin(pCtx.PluginID)
if !exists {
return backendplugin.ErrPluginNotRegistered
}
@@ -419,8 +407,8 @@ func flushStream(plugin backendplugin.Plugin, stream callResourceClientResponseS
}
func (m *PluginManager) CollectMetrics(ctx context.Context, pluginID string) (*backend.CollectMetricsResult, error) {
p := m.Plugin(pluginID)
if p == nil {
p, exists := m.plugin(pluginID)
if !exists {
return nil, backendplugin.ErrPluginNotRegistered
}
@@ -450,8 +438,8 @@ func (m *PluginManager) CheckHealth(ctx context.Context, req *backend.CheckHealt
}, nil
}
p := m.Plugin(req.PluginContext.PluginID)
if p == nil {
p, exists := m.plugin(req.PluginContext.PluginID)
if !exists {
return nil, backendplugin.ErrPluginNotRegistered
}
@@ -504,96 +492,14 @@ func (m *PluginManager) RunStream(ctx context.Context, req *backend.RunStreamReq
}
func (m *PluginManager) isRegistered(pluginID string) bool {
p := m.Plugin(pluginID)
if p == nil {
p, exists := m.plugin(pluginID)
if !exists {
return false
}
return !p.IsDecommissioned()
}
func (m *PluginManager) Add(ctx context.Context, pluginID, version string, opts plugins.AddOpts) error {
var pluginZipURL string
if opts.PluginRepoURL == "" {
opts.PluginRepoURL = grafanaComURL
}
plugin := m.Plugin(pluginID)
if plugin != nil {
if !plugin.IsExternalPlugin() {
return plugins.ErrInstallCorePlugin
}
if plugin.Info.Version == version {
return plugins.DuplicateError{
PluginID: plugin.ID,
ExistingPluginDir: plugin.PluginDir,
}
}
// get plugin update information to confirm if upgrading is possible
updateInfo, err := m.pluginInstaller.GetUpdateInfo(ctx, pluginID, version, opts.PluginRepoURL)
if err != nil {
return err
}
pluginZipURL = updateInfo.PluginZipURL
// remove existing installation of plugin
err = m.Remove(ctx, plugin.ID)
if err != nil {
return err
}
}
if opts.PluginInstallDir == "" {
opts.PluginInstallDir = m.cfg.PluginsPath
}
if opts.PluginZipURL == "" {
opts.PluginZipURL = pluginZipURL
}
err := m.pluginInstaller.Install(ctx, pluginID, version, opts.PluginInstallDir, opts.PluginZipURL, opts.PluginRepoURL)
if err != nil {
return err
}
err = m.loadPlugins(opts.PluginInstallDir)
if err != nil {
return err
}
return nil
}
func (m *PluginManager) Remove(ctx context.Context, pluginID string) error {
plugin := m.Plugin(pluginID)
if plugin == nil {
return plugins.ErrPluginNotInstalled
}
if !plugin.IsExternalPlugin() {
return plugins.ErrUninstallCorePlugin
}
// extra security check to ensure we only remove plugins that are located in the configured plugins directory
path, err := filepath.Rel(m.cfg.PluginsPath, plugin.PluginDir)
if err != nil || strings.HasPrefix(path, ".."+string(filepath.Separator)) {
return plugins.ErrUninstallOutsideOfPluginDir
}
if m.isRegistered(pluginID) {
err := m.unregisterAndStop(ctx, plugin)
if err != nil {
return err
}
}
return m.pluginInstaller.Uninstall(ctx, plugin.PluginDir)
}
func (m *PluginManager) LoadAndRegister(pluginID string, factory backendplugin.PluginFactoryFunc) error {
if m.isRegistered(pluginID) {
return fmt.Errorf("backend plugin %s already registered", pluginID)
@@ -620,9 +526,9 @@ func (m *PluginManager) LoadAndRegister(pluginID string, factory backendplugin.P
}
func (m *PluginManager) Routes() []*plugins.StaticRoute {
staticRoutes := []*plugins.StaticRoute{}
staticRoutes := make([]*plugins.StaticRoute, 0)
for _, p := range m.Plugins() {
for _, p := range m.plugins() {
if p.StaticRoute() != nil {
staticRoutes = append(staticRoutes, p.StaticRoute())
}
@@ -644,18 +550,16 @@ func (m *PluginManager) registerAndStart(ctx context.Context, plugin *plugins.Pl
}
func (m *PluginManager) register(p *plugins.Plugin) error {
m.pluginsMu.Lock()
defer m.pluginsMu.Unlock()
pluginID := p.ID
if _, exists := m.plugins[pluginID]; exists {
return fmt.Errorf("plugin %s already registered", pluginID)
if m.isRegistered(p.ID) {
return fmt.Errorf("plugin %s is already registered", p.ID)
}
m.plugins[pluginID] = p
m.pluginsMu.Lock()
m.store[p.ID] = p
m.pluginsMu.Unlock()
if !p.IsCorePlugin() {
m.log.Info("Plugin registered", "pluginId", pluginID)
m.log.Info("Plugin registered", "pluginId", p.ID)
}
return nil
@@ -663,6 +567,9 @@ func (m *PluginManager) register(p *plugins.Plugin) error {
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
}
@@ -671,7 +578,7 @@ func (m *PluginManager) unregisterAndStop(ctx context.Context, p *plugins.Plugin
return err
}
delete(m.plugins, p.ID)
delete(m.store, p.ID)
m.log.Debug("Plugin unregistered", "pluginId", p.ID)
return nil
@@ -742,12 +649,10 @@ func restartKilledProcess(ctx context.Context, p *plugins.Plugin) error {
}
}
// stop stops a backend plugin process
func (m *PluginManager) stop(ctx context.Context) {
m.pluginsMu.RLock()
defer m.pluginsMu.RUnlock()
// shutdown stops all backend plugin processes
func (m *PluginManager) shutdown(ctx context.Context) {
var wg sync.WaitGroup
for _, p := range m.plugins {
for _, p := range m.plugins() {
wg.Add(1)
go func(p backendplugin.Plugin, ctx context.Context) {
defer wg.Done()

View File

@@ -1,6 +1,7 @@
package manager
import (
"context"
"path/filepath"
"strings"
"testing"
@@ -101,38 +102,44 @@ func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) {
"test-app": {},
}
panels := pm.Plugins(plugins.Panel)
panels := pm.Plugins(context.Background(), plugins.Panel)
assert.Equal(t, len(expPanels), len(panels))
for _, p := range panels {
require.NotNil(t, pm.Plugin(p.ID))
p, exists := pm.Plugin(context.Background(), p.ID)
require.NotEqual(t, plugins.PluginDTO{}, p)
assert.True(t, exists)
assert.Contains(t, expPanels, p.ID)
assert.Contains(t, pm.registeredPlugins(), p.ID)
}
dataSources := pm.Plugins(plugins.DataSource)
dataSources := pm.Plugins(context.Background(), plugins.DataSource)
assert.Equal(t, len(expDataSources), len(dataSources))
for _, ds := range dataSources {
require.NotNil(t, pm.Plugin(ds.ID))
p, exists := pm.Plugin(context.Background(), ds.ID)
require.NotEqual(t, plugins.PluginDTO{}, p)
assert.True(t, exists)
assert.Contains(t, expDataSources, ds.ID)
assert.Contains(t, pm.registeredPlugins(), ds.ID)
}
apps := pm.Plugins(plugins.App)
apps := pm.Plugins(context.Background(), plugins.App)
assert.Equal(t, len(expApps), len(apps))
for _, app := range apps {
require.NotNil(t, pm.Plugin(app.ID))
require.Contains(t, expApps, app.ID)
p, exists := pm.Plugin(context.Background(), app.ID)
require.NotEqual(t, plugins.PluginDTO{}, p)
assert.True(t, exists)
assert.Contains(t, expApps, app.ID)
assert.Contains(t, pm.registeredPlugins(), app.ID)
}
assert.Equal(t, len(expPanels)+len(expDataSources)+len(expApps), len(pm.Plugins()))
assert.Equal(t, len(expPanels)+len(expDataSources)+len(expApps), len(pm.Plugins(context.Background())))
}
func verifyBundledPlugins(t *testing.T, pm *PluginManager) {
t.Helper()
dsPlugins := make(map[string]struct{})
for _, p := range pm.Plugins(plugins.DataSource) {
for _, p := range pm.Plugins(context.Background(), plugins.DataSource) {
dsPlugins[p.ID] = struct{}{}
}
@@ -141,26 +148,30 @@ func verifyBundledPlugins(t *testing.T, pm *PluginManager) {
pluginRoutes[r.PluginID] = r
}
assert.NotNil(t, pm.Plugin("input"))
inputPlugin, exists := pm.Plugin(context.Background(), "input")
require.NotEqual(t, plugins.PluginDTO{}, inputPlugin)
assert.True(t, exists)
assert.NotNil(t, dsPlugins["input"])
for _, pluginID := range []string{"input"} {
assert.Contains(t, pluginRoutes, pluginID)
assert.True(t, strings.HasPrefix(pluginRoutes[pluginID].Directory, pm.Plugin("input").PluginDir))
assert.True(t, strings.HasPrefix(pluginRoutes[pluginID].Directory, inputPlugin.PluginDir))
}
}
func verifyPluginStaticRoutes(t *testing.T, pm *PluginManager) {
pluginRoutes := make(map[string]*plugins.StaticRoute)
routes := make(map[string]*plugins.StaticRoute)
for _, route := range pm.Routes() {
pluginRoutes[route.PluginID] = route
routes[route.PluginID] = route
}
assert.Len(t, pluginRoutes, 2)
assert.Len(t, routes, 2)
assert.Contains(t, pluginRoutes, "input")
assert.Equal(t, pluginRoutes["input"].Directory, pm.Plugin("input").PluginDir)
inputPlugin, _ := pm.Plugin(context.Background(), "input")
assert.NotNil(t, routes["input"])
assert.Equal(t, routes["input"].Directory, inputPlugin.PluginDir)
assert.Contains(t, pluginRoutes, "test-app")
assert.Equal(t, pluginRoutes["test-app"].Directory, pm.Plugin("test-app").PluginDir)
testAppPlugin, _ := pm.Plugin(context.Background(), "test-app")
assert.Contains(t, routes, "test-app")
assert.Equal(t, routes["test-app"].Directory, testAppPlugin.PluginDir)
}

View File

@@ -73,8 +73,11 @@ func TestPluginManager_loadPlugins(t *testing.T) {
assert.Equal(t, 0, pc.stopCount)
assert.False(t, pc.exited)
assert.False(t, pc.decommissioned)
assert.Equal(t, p, pm.Plugin(testPluginID))
assert.Len(t, pm.Plugins(), 1)
testPlugin, exists := pm.Plugin(context.Background(), testPluginID)
assert.True(t, exists)
assert.Equal(t, p.ToDTO(), testPlugin)
assert.Len(t, pm.Plugins(context.Background()), 1)
verifyNoPluginErrors(t, pm)
})
@@ -96,8 +99,11 @@ func TestPluginManager_loadPlugins(t *testing.T) {
assert.Equal(t, 0, pc.stopCount)
assert.False(t, pc.exited)
assert.False(t, pc.decommissioned)
assert.Equal(t, p, pm.Plugin(testPluginID))
assert.Len(t, pm.Plugins(), 1)
testPlugin, exists := pm.Plugin(context.Background(), testPluginID)
assert.True(t, exists)
assert.Equal(t, p.ToDTO(), testPlugin)
assert.Len(t, pm.Plugins(context.Background()), 1)
verifyNoPluginErrors(t, pm)
})
@@ -119,8 +125,11 @@ func TestPluginManager_loadPlugins(t *testing.T) {
assert.Equal(t, 0, pc.stopCount)
assert.False(t, pc.exited)
assert.False(t, pc.decommissioned)
assert.Equal(t, p, pm.Plugin(testPluginID))
assert.Len(t, pm.Plugins(), 1)
testPlugin, exists := pm.Plugin(context.Background(), testPluginID)
assert.True(t, exists)
assert.Equal(t, p.ToDTO(), testPlugin)
assert.Len(t, pm.Plugins(context.Background()), 1)
verifyNoPluginErrors(t, pm)
})
@@ -142,8 +151,11 @@ func TestPluginManager_loadPlugins(t *testing.T) {
assert.Equal(t, 0, pc.stopCount)
assert.False(t, pc.exited)
assert.False(t, pc.decommissioned)
assert.Equal(t, p, pm.Plugin(testPluginID))
assert.Len(t, pm.Plugins(), 1)
testPlugin, exists := pm.Plugin(context.Background(), testPluginID)
assert.True(t, exists)
assert.Equal(t, p.ToDTO(), testPlugin)
assert.Len(t, pm.Plugins(context.Background()), 1)
verifyNoPluginErrors(t, pm)
})
@@ -179,8 +191,11 @@ func TestPluginManager_Installer(t *testing.T) {
assert.Equal(t, 0, pc.stopCount)
assert.False(t, pc.exited)
assert.False(t, pc.decommissioned)
assert.Equal(t, p, pm.Plugin(testPluginID))
assert.Len(t, pm.Plugins(), 1)
testPlugin, exists := pm.Plugin(context.Background(), testPluginID)
assert.True(t, exists)
assert.Equal(t, p.ToDTO(), testPlugin)
assert.Len(t, pm.Plugins(context.Background()), 1)
t.Run("Won't install if already installed", func(t *testing.T) {
err := pm.Add(context.Background(), testPluginID, "1.0.0", plugins.AddOpts{})
@@ -208,8 +223,11 @@ func TestPluginManager_Installer(t *testing.T) {
assert.Equal(t, 0, pc.stopCount)
assert.False(t, pc.exited)
assert.False(t, pc.decommissioned)
assert.Equal(t, p, pm.Plugin(testPluginID))
assert.Len(t, pm.Plugins(), 1)
testPlugin, exists := pm.Plugin(context.Background(), testPluginID)
assert.True(t, exists)
assert.Equal(t, p.ToDTO(), testPlugin)
assert.Len(t, pm.Plugins(context.Background()), 1)
})
t.Run("Uninstall", func(t *testing.T) {
@@ -219,7 +237,9 @@ func TestPluginManager_Installer(t *testing.T) {
assert.Equal(t, 2, i.installCount)
assert.Equal(t, 2, i.uninstallCount)
assert.Nil(t, pm.Plugin(p.ID))
p, exists := pm.Plugin(context.Background(), p.ID)
assert.False(t, exists)
assert.Equal(t, plugins.PluginDTO{}, p)
assert.Len(t, pm.Routes(), 0)
t.Run("Won't uninstall if not installed", func(t *testing.T) {
@@ -246,8 +266,11 @@ func TestPluginManager_Installer(t *testing.T) {
assert.Equal(t, 0, pc.stopCount)
assert.False(t, pc.exited)
assert.False(t, pc.decommissioned)
assert.Equal(t, p, pm.Plugin(testPluginID))
assert.Len(t, pm.Plugins(), 1)
testPlugin, exists := pm.Plugin(context.Background(), testPluginID)
assert.True(t, exists)
assert.Equal(t, p.ToDTO(), testPlugin)
assert.Len(t, pm.Plugins(context.Background()), 1)
verifyNoPluginErrors(t, pm)
@@ -277,8 +300,11 @@ func TestPluginManager_Installer(t *testing.T) {
assert.Equal(t, 0, pc.stopCount)
assert.False(t, pc.exited)
assert.False(t, pc.decommissioned)
assert.Equal(t, p, pm.Plugin(testPluginID))
assert.Len(t, pm.Plugins(), 1)
testPlugin, exists := pm.Plugin(context.Background(), testPluginID)
assert.True(t, exists)
assert.Equal(t, p.ToDTO(), testPlugin)
assert.Len(t, pm.Plugins(context.Background()), 1)
verifyNoPluginErrors(t, pm)
@@ -301,7 +327,9 @@ func TestPluginManager_lifecycle_managed(t *testing.T) {
require.NotNil(t, ctx.plugin)
require.Equal(t, testPluginID, ctx.plugin.ID)
require.Equal(t, 1, ctx.pluginClient.startCount)
require.NotNil(t, ctx.manager.Plugin(testPluginID))
testPlugin, exists := ctx.manager.Plugin(context.Background(), testPluginID)
assert.True(t, exists)
require.NotNil(t, testPlugin)
t.Run("Should not be able to register an already registered plugin", func(t *testing.T) {
err := ctx.manager.registerAndStart(context.Background(), ctx.plugin)
@@ -564,7 +592,7 @@ func newScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *managerS
}
func verifyNoPluginErrors(t *testing.T, pm *PluginManager) {
for _, plugin := range pm.Plugins() {
for _, plugin := range pm.Plugins(context.Background()) {
assert.Nil(t, plugin.SignatureError)
}
}

View File

@@ -0,0 +1,120 @@
package manager
import (
"context"
"path/filepath"
"strings"
"github.com/grafana/grafana/pkg/plugins"
)
func (m *PluginManager) Plugin(_ context.Context, pluginID string) (plugins.PluginDTO, bool) {
p, exists := m.plugin(pluginID)
if !exists {
return plugins.PluginDTO{}, false
}
return p.ToDTO(), true
}
func (m *PluginManager) Plugins(_ context.Context, pluginTypes ...plugins.Type) []plugins.PluginDTO {
// if no types passed, assume all
if len(pluginTypes) == 0 {
pluginTypes = plugins.PluginTypes
}
var requestedTypes = make(map[plugins.Type]struct{})
for _, pt := range pluginTypes {
requestedTypes[pt] = struct{}{}
}
pluginsList := make([]plugins.PluginDTO, 0)
for _, p := range m.plugins() {
if _, exists := requestedTypes[p.Type]; exists {
pluginsList = append(pluginsList, p.ToDTO())
}
}
return pluginsList
}
func (m *PluginManager) Add(ctx context.Context, pluginID, version string, opts plugins.AddOpts) error {
var pluginZipURL string
if opts.PluginRepoURL == "" {
opts.PluginRepoURL = grafanaComURL
}
if plugin, exists := m.plugin(pluginID); exists {
if !plugin.IsExternalPlugin() {
return plugins.ErrInstallCorePlugin
}
if plugin.Info.Version == version {
return plugins.DuplicateError{
PluginID: plugin.ID,
ExistingPluginDir: plugin.PluginDir,
}
}
// get plugin update information to confirm if upgrading is possible
updateInfo, err := m.pluginInstaller.GetUpdateInfo(ctx, pluginID, version, opts.PluginRepoURL)
if err != nil {
return err
}
pluginZipURL = updateInfo.PluginZipURL
// remove existing installation of plugin
err = m.Remove(ctx, plugin.ID)
if err != nil {
return err
}
}
if opts.PluginInstallDir == "" {
opts.PluginInstallDir = m.cfg.PluginsPath
}
if opts.PluginZipURL == "" {
opts.PluginZipURL = pluginZipURL
}
err := m.pluginInstaller.Install(ctx, pluginID, version, opts.PluginInstallDir, opts.PluginZipURL, opts.PluginRepoURL)
if err != nil {
return err
}
err = m.loadPlugins(opts.PluginInstallDir)
if err != nil {
return err
}
return nil
}
func (m *PluginManager) Remove(ctx context.Context, pluginID string) error {
plugin, exists := m.plugin(pluginID)
if !exists {
return plugins.ErrPluginNotInstalled
}
if !plugin.IsExternalPlugin() {
return plugins.ErrUninstallCorePlugin
}
// extra security check to ensure we only remove plugins that are located in the configured plugins directory
path, err := filepath.Rel(m.cfg.PluginsPath, plugin.PluginDir)
if err != nil || strings.HasPrefix(path, ".."+string(filepath.Separator)) {
return plugins.ErrUninstallOutsideOfPluginDir
}
if m.isRegistered(pluginID) {
err := m.unregisterAndStop(ctx, plugin)
if err != nil {
return err
}
}
return m.pluginInstaller.Uninstall(ctx, plugin.PluginDir)
}

View File

@@ -1,6 +1,7 @@
package manager
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
@@ -26,8 +27,8 @@ func (m *PluginManager) checkForUpdates() {
m.log.Debug("Checking for updates")
pluginSlugs := m.externalPluginIDsAsCSV()
resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + m.cfg.BuildVersion)
pluginIDs := m.pluginsEligibleForVersionCheck()
resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + strings.Join(pluginIDs, ",") + "&grafanaVersion=" + m.cfg.BuildVersion)
if err != nil {
m.log.Debug("Failed to get plugins repo from grafana.com", "error", err.Error())
return
@@ -51,7 +52,7 @@ func (m *PluginManager) checkForUpdates() {
return
}
for _, localP := range m.Plugins() {
for _, localP := range m.Plugins(context.TODO()) {
for _, gcomP := range gcomPlugins {
if gcomP.Slug == localP.ID {
localP.GrafanaComVersion = gcomP.Version
@@ -69,9 +70,9 @@ func (m *PluginManager) checkForUpdates() {
}
}
func (m *PluginManager) externalPluginIDsAsCSV() string {
func (m *PluginManager) pluginsEligibleForVersionCheck() []string {
var result []string
for _, p := range m.plugins {
for _, p := range m.plugins() {
if p.IsCorePlugin() {
continue
}
@@ -79,5 +80,5 @@ func (m *PluginManager) externalPluginIDsAsCSV() string {
result = append(result, p.ID)
}
return strings.Join(result, ",")
return result
}

View File

@@ -49,8 +49,8 @@ type Provider struct {
// returned context.
func (p *Provider) Get(ctx context.Context, pluginID string, datasourceUID string, user *models.SignedInUser, skipCache bool) (backend.PluginContext, bool, error) {
pc := backend.PluginContext{}
plugin := p.pluginStore.Plugin(pluginID)
if plugin == nil {
plugin, exists := p.pluginStore.Plugin(ctx, pluginID)
if !exists {
return pc, false, nil
}

View File

@@ -2,6 +2,7 @@ package plugindashboards
import (
"context"
"fmt"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
@@ -46,7 +47,7 @@ func (s *Service) updateAppDashboards() {
continue
}
if pluginDef := s.pluginStore.Plugin(pluginSetting.PluginId); pluginDef != nil {
if pluginDef, exists := s.pluginStore.Plugin(context.Background(), pluginSetting.PluginId); exists {
if pluginDef.Info.Version != pluginSetting.PluginVersion {
s.syncPluginDashboards(context.Background(), pluginDef, pluginSetting.OrgId)
}
@@ -54,11 +55,11 @@ func (s *Service) updateAppDashboards() {
}
}
func (s *Service) syncPluginDashboards(ctx context.Context, pluginDef *plugins.Plugin, orgID int64) {
s.logger.Info("Syncing plugin dashboards to DB", "pluginId", pluginDef.ID)
func (s *Service) syncPluginDashboards(ctx context.Context, plugin plugins.PluginDTO, orgID int64) {
s.logger.Info("Syncing plugin dashboards to DB", "pluginId", plugin.ID)
// Get plugin dashboards
dashboards, err := s.pluginDashboardManager.GetPluginDashboards(orgID, pluginDef.ID)
dashboards, err := s.pluginDashboardManager.GetPluginDashboards(orgID, plugin.ID)
if err != nil {
s.logger.Error("Failed to load app dashboards", "error", err)
return
@@ -68,11 +69,11 @@ func (s *Service) syncPluginDashboards(ctx context.Context, pluginDef *plugins.P
for _, dash := range dashboards {
// remove removed ones
if dash.Removed {
s.logger.Info("Deleting plugin dashboard", "pluginId", pluginDef.ID, "dashboard", dash.Slug)
s.logger.Info("Deleting plugin dashboard", "pluginId", plugin.ID, "dashboard", dash.Slug)
deleteCmd := models.DeleteDashboardCommand{OrgId: orgID, Id: dash.DashboardId}
if err := bus.Dispatch(&deleteCmd); err != nil {
s.logger.Error("Failed to auto update app dashboard", "pluginId", pluginDef.ID, "error", err)
s.logger.Error("Failed to auto update app dashboard", "pluginId", plugin.ID, "error", err)
return
}
@@ -82,14 +83,14 @@ func (s *Service) syncPluginDashboards(ctx context.Context, pluginDef *plugins.P
// update updated ones
if dash.ImportedRevision != dash.Revision {
if err := s.autoUpdateAppDashboard(ctx, dash, orgID); err != nil {
s.logger.Error("Failed to auto update app dashboard", "pluginId", pluginDef.ID, "error", err)
s.logger.Error("Failed to auto update app dashboard", "pluginId", plugin.ID, "error", err)
return
}
}
}
// update version in plugin_setting table to mark that we have processed the update
query := models.GetPluginSettingByIdQuery{PluginId: pluginDef.ID, OrgId: orgID}
query := models.GetPluginSettingByIdQuery{PluginId: plugin.ID, OrgId: orgID}
if err := bus.DispatchCtx(ctx, &query); err != nil {
s.logger.Error("Failed to read plugin setting by ID", "error", err)
return
@@ -99,7 +100,7 @@ func (s *Service) syncPluginDashboards(ctx context.Context, pluginDef *plugins.P
cmd := models.UpdatePluginSettingVersionCmd{
OrgId: appSetting.OrgId,
PluginId: appSetting.PluginId,
PluginVersion: pluginDef.Info.Version,
PluginVersion: plugin.Info.Version,
}
if err := bus.DispatchCtx(ctx, &cmd); err != nil {
@@ -111,7 +112,12 @@ func (s *Service) handlePluginStateChanged(event *models.PluginStateChangedEvent
s.logger.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled)
if event.Enabled {
s.syncPluginDashboards(context.TODO(), s.pluginStore.Plugin(event.PluginId), event.OrgId)
p, exists := s.pluginStore.Plugin(context.TODO(), event.PluginId)
if !exists {
return fmt.Errorf("plugin %s not found. Could not sync plugin dashboards", event.PluginId)
}
s.syncPluginDashboards(context.TODO(), p, event.OrgId)
} else {
query := models.GetDashboardsByPluginIdQuery{PluginId: event.PluginId, OrgId: event.OrgId}
if err := bus.DispatchCtx(context.TODO(), &query); err != nil {

View File

@@ -45,6 +45,65 @@ type Plugin struct {
log log.Logger
}
type PluginDTO struct {
JSONData
PluginDir string
Class Class
// App fields
IncludedInAppID string
DefaultNavURL string
Pinned bool
// Signature fields
Signature SignatureStatus
SignatureType SignatureType
SignatureOrg string
SignedFiles PluginFiles
SignatureError *SignatureError
// GCOM update checker fields
GrafanaComVersion string
GrafanaComHasUpdate bool
// SystemJS fields
Module string
BaseURL string
// temporary
backend.StreamHandler
}
func (p PluginDTO) SupportsStreaming() bool {
return p.StreamHandler != nil
}
func (p PluginDTO) IsApp() bool {
return p.Type == "app"
}
func (p PluginDTO) IsCorePlugin() bool {
return p.Class == Core
}
func (p PluginDTO) IncludedInSignature(file string) bool {
// permit Core plugin files
if p.IsCorePlugin() {
return true
}
// permit when no signed files (no MANIFEST)
if p.SignedFiles == nil {
return true
}
if _, exists := p.SignedFiles[file]; !exists {
return false
}
return true
}
// JSONData represents the plugin's plugin.json
type JSONData struct {
// Common settings
@@ -252,6 +311,29 @@ type PluginClient interface {
backend.StreamHandler
}
func (p *Plugin) ToDTO() PluginDTO {
c, _ := p.Client()
return PluginDTO{
JSONData: p.JSONData,
PluginDir: p.PluginDir,
Class: p.Class,
IncludedInAppID: p.IncludedInAppID,
DefaultNavURL: p.DefaultNavURL,
Pinned: p.Pinned,
Signature: p.Signature,
SignatureType: p.SignatureType,
SignatureOrg: p.SignatureOrg,
SignedFiles: p.SignedFiles,
SignatureError: p.SignatureError,
GrafanaComVersion: p.GrafanaComVersion,
GrafanaComHasUpdate: p.GrafanaComHasUpdate,
Module: p.Module,
BaseURL: p.BaseURL,
StreamHandler: c,
}
}
func (p *Plugin) StaticRoute() *StaticRoute {
if p.IsCorePlugin() {
return nil
@@ -288,33 +370,6 @@ func (p *Plugin) IsExternalPlugin() bool {
return p.Class == External
}
func (p *Plugin) SupportsStreaming() bool {
pluginClient, ok := p.Client()
if !ok {
return false
}
_, ok = pluginClient.(backend.StreamHandler)
return ok
}
func (p *Plugin) IncludedInSignature(file string) bool {
// permit Core plugin files
if p.IsCorePlugin() {
return true
}
// permit when no signed files (no MANIFEST)
if p.SignedFiles == nil {
return true
}
if _, exists := p.SignedFiles[file]; !exists {
return false
}
return true
}
type Class string
const (