PluginManager: Make remaining plugin state non-global (#32094)

* PluginDashboards: Use plugin manager interface

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* PluginManager: Make panels non-global

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* PluginManager: Make apps non-global

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* PluginManager: Make static routes non-global

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>

* PluginManager: Make pluginTypes non-global

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
pull/32104/head^2
Arve Knudsen 4 years ago committed by GitHub
parent 1454c3723d
commit a2eda798e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      pkg/api/api.go
  2. 3
      pkg/api/app_routes.go
  3. 6
      pkg/api/fakes.go
  4. 125
      pkg/api/frontend_logging_test.go
  5. 11
      pkg/api/frontendlogging/source_maps.go
  6. 3
      pkg/api/http_server.go
  7. 14
      pkg/api/plugins.go
  8. 5
      pkg/infra/usagestats/usage_stats.go
  9. 9
      pkg/infra/usagestats/usage_stats_test.go
  10. 15
      pkg/plugins/ifaces.go
  11. 14
      pkg/plugins/manager/dashboard_import_test.go
  12. 14
      pkg/plugins/manager/dashboards.go
  13. 86
      pkg/plugins/manager/dashboards_test.go
  14. 92
      pkg/plugins/manager/manager.go
  15. 23
      pkg/plugins/manager/manager_test.go
  16. 10
      pkg/plugins/manager/queries.go
  17. 7
      pkg/plugins/plugindashboards/service.go
  18. 2
      pkg/server/server.go
  19. 13
      pkg/services/provisioning/plugins/config_reader.go
  20. 32
      pkg/services/provisioning/plugins/config_reader_test.go
  21. 16
      pkg/services/provisioning/plugins/plugin_provisioner.go
  22. 7
      pkg/services/provisioning/provisioning.go

@ -275,7 +275,7 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
pluginRoute.Get("/:pluginId/dashboards/", routing.Wrap(hs.GetPluginDashboards))
pluginRoute.Post("/:pluginId/settings", bind(models.UpdatePluginSettingCmd{}), routing.Wrap(UpdatePluginSetting))
pluginRoute.Post("/:pluginId/settings", bind(models.UpdatePluginSettingCmd{}), routing.Wrap(hs.UpdatePluginSetting))
pluginRoute.Get("/:pluginId/metrics", routing.Wrap(hs.CollectPluginMetrics))
}, reqOrgAdmin)
@ -447,6 +447,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Delete("/api/snapshots/:key", reqEditorRole, routing.Wrap(DeleteDashboardSnapshot))
// Frontend logs
sourceMapStore := frontendlogging.NewSourceMapStore(hs.Cfg, frontendlogging.ReadSourceMapFromFS)
r.Post("/log", middleware.RateLimit(hs.Cfg.Sentry.EndpointRPS, hs.Cfg.Sentry.EndpointBurst, time.Now), bind(frontendlogging.FrontendSentryEvent{}), routing.Wrap(NewFrontendLogMessageHandler(sourceMapStore)))
sourceMapStore := frontendlogging.NewSourceMapStore(hs.Cfg, hs.PluginManager, frontendlogging.ReadSourceMapFromFS)
r.Post("/log", middleware.RateLimit(hs.Cfg.Sentry.EndpointRPS, hs.Cfg.Sentry.EndpointBurst, time.Now),
bind(frontendlogging.FrontendSentryEvent{}), routing.Wrap(NewFrontendLogMessageHandler(sourceMapStore)))
}

@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/util"
macaron "gopkg.in/macaron.v1"
)
@ -32,7 +31,7 @@ func (hs *HTTPServer) initAppPluginRoutes(r *macaron.Macaron) {
TLSHandshakeTimeout: 10 * time.Second,
}
for _, plugin := range manager.Apps {
for _, plugin := range hs.PluginManager.Apps() {
for _, route := range plugin.Routes {
url := util.JoinURLFragments("/api/plugin-proxy/"+plugin.Id, route.Path)
handlers := make([]macaron.Handler, 0)

@ -4,6 +4,8 @@ import "github.com/grafana/grafana/pkg/plugins"
type fakePluginManager struct {
plugins.Manager
staticRoutes []*plugins.PluginStaticRoute
}
func (pm *fakePluginManager) GetPlugin(id string) *plugins.PluginBase {
@ -17,3 +19,7 @@ func (pm *fakePluginManager) GetDataSource(id string) *plugins.DataSourcePlugin
func (pm *fakePluginManager) Renderer() *plugins.RendererPlugin {
return nil
}
func (pm *fakePluginManager) StaticRoutes() []*plugins.PluginStaticRoute {
return pm.staticRoutes
}

@ -16,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/setting"
log "github.com/inconshreveable/log15"
@ -71,7 +70,17 @@ func logSentryEventScenario(t *testing.T, desc string, event frontendlogging.Fro
return nil, os.ErrNotExist
}
sourceMapStore := frontendlogging.NewSourceMapStore(cfg, readSourceMap)
// fake plugin route so we will try to find a source map there
pm := fakePluginManager{
staticRoutes: []*plugins.PluginStaticRoute{
{
Directory: "/usr/local/telepathic-panel",
PluginId: "telepathic",
},
},
}
sourceMapStore := frontendlogging.NewSourceMapStore(cfg, &pm, readSourceMap)
loggingHandler := NewFrontendLogMessageHandler(sourceMapStore)
@ -90,12 +99,6 @@ func TestFrontendLoggingEndpoint(t *testing.T) {
ts, err := time.Parse("2006-01-02T15:04:05.000Z", "2020-10-22T06:29:29.078Z")
require.NoError(t, err)
// fake plugin route so we will try to find a source map there. I can't believe I can do this
manager.StaticRoutes = append(manager.StaticRoutes, &plugins.PluginStaticRoute{
Directory: "/usr/local/telepathic-panel",
PluginId: "telepathic",
})
t.Run("FrontendLoggingEndpoint", func(t *testing.T) {
request := sentry.Request{
URL: "http://localhost:3000/",
@ -144,19 +147,20 @@ func TestFrontendLoggingEndpoint(t *testing.T) {
},
}
logSentryEventScenario(t, "Should log received error event", errorEvent, func(sc *scenarioContext, logs []*log.Record, sourceMapReads []SourceMapReadRecord) {
assert.Equal(t, 200, sc.resp.Code)
assert.Len(t, logs, 1)
assertContextContains(t, logs[0], "logger", "frontend")
assertContextContains(t, logs[0], "url", errorEvent.Request.URL)
assertContextContains(t, logs[0], "user_agent", errorEvent.Request.Headers["User-Agent"])
assertContextContains(t, logs[0], "event_id", errorEvent.EventID)
assertContextContains(t, logs[0], "original_timestamp", errorEvent.Timestamp)
assertContextContains(t, logs[0], "stacktrace", `UserError: Please replace user and try again
logSentryEventScenario(t, "Should log received error event", errorEvent,
func(sc *scenarioContext, logs []*log.Record, sourceMapReads []SourceMapReadRecord) {
assert.Equal(t, 200, sc.resp.Code)
assert.Len(t, logs, 1)
assertContextContains(t, logs[0], "logger", "frontend")
assertContextContains(t, logs[0], "url", errorEvent.Request.URL)
assertContextContains(t, logs[0], "user_agent", errorEvent.Request.Headers["User-Agent"])
assertContextContains(t, logs[0], "event_id", errorEvent.EventID)
assertContextContains(t, logs[0], "original_timestamp", errorEvent.Timestamp)
assertContextContains(t, logs[0], "stacktrace", `UserError: Please replace user and try again
at foofn (foo.js:123:23)
at barfn (bar.js:113:231)`)
assert.NotContains(t, logs[0].Ctx, "context")
})
assert.NotContains(t, logs[0].Ctx, "context")
})
messageEvent := frontendlogging.FrontendSentryEvent{
Event: &sentry.Event{
@ -170,21 +174,22 @@ func TestFrontendLoggingEndpoint(t *testing.T) {
Exception: nil,
}
logSentryEventScenario(t, "Should log received message event", messageEvent, func(sc *scenarioContext, logs []*log.Record, sourceMapReads []SourceMapReadRecord) {
assert.Equal(t, 200, sc.resp.Code)
assert.Len(t, logs, 1)
assert.Equal(t, "hello world", logs[0].Msg)
assert.Equal(t, log.LvlInfo, logs[0].Lvl)
assertContextContains(t, logs[0], "logger", "frontend")
assertContextContains(t, logs[0], "url", messageEvent.Request.URL)
assertContextContains(t, logs[0], "user_agent", messageEvent.Request.Headers["User-Agent"])
assertContextContains(t, logs[0], "event_id", messageEvent.EventID)
assertContextContains(t, logs[0], "original_timestamp", messageEvent.Timestamp)
assert.NotContains(t, logs[0].Ctx, "stacktrace")
assert.NotContains(t, logs[0].Ctx, "context")
assertContextContains(t, logs[0], "user_email", user.Email)
assertContextContains(t, logs[0], "user_id", user.ID)
})
logSentryEventScenario(t, "Should log received message event", messageEvent,
func(sc *scenarioContext, logs []*log.Record, sourceMapReads []SourceMapReadRecord) {
assert.Equal(t, 200, sc.resp.Code)
assert.Len(t, logs, 1)
assert.Equal(t, "hello world", logs[0].Msg)
assert.Equal(t, log.LvlInfo, logs[0].Lvl)
assertContextContains(t, logs[0], "logger", "frontend")
assertContextContains(t, logs[0], "url", messageEvent.Request.URL)
assertContextContains(t, logs[0], "user_agent", messageEvent.Request.Headers["User-Agent"])
assertContextContains(t, logs[0], "event_id", messageEvent.EventID)
assertContextContains(t, logs[0], "original_timestamp", messageEvent.Timestamp)
assert.NotContains(t, logs[0].Ctx, "stacktrace")
assert.NotContains(t, logs[0].Ctx, "context")
assertContextContains(t, logs[0], "user_email", user.Email)
assertContextContains(t, logs[0], "user_id", user.ID)
})
eventWithContext := frontendlogging.FrontendSentryEvent{
Event: &sentry.Event{
@ -205,13 +210,14 @@ func TestFrontendLoggingEndpoint(t *testing.T) {
Exception: nil,
}
logSentryEventScenario(t, "Should log event context", eventWithContext, func(sc *scenarioContext, logs []*log.Record, sourceMapReads []SourceMapReadRecord) {
assert.Equal(t, 200, sc.resp.Code)
assert.Len(t, logs, 1)
assertContextContains(t, logs[0], "context_foo_one", "two")
assertContextContains(t, logs[0], "context_foo_three", "4")
assertContextContains(t, logs[0], "context_bar", "baz")
})
logSentryEventScenario(t, "Should log event context", eventWithContext,
func(sc *scenarioContext, logs []*log.Record, sourceMapReads []SourceMapReadRecord) {
assert.Equal(t, 200, sc.resp.Code)
assert.Len(t, logs, 1)
assertContextContains(t, logs[0], "context_foo_one", "two")
assertContextContains(t, logs[0], "context_foo_three", "4")
assertContextContains(t, logs[0], "context_bar", "baz")
})
errorEventForSourceMapping := frontendlogging.FrontendSentryEvent{
Event: &event,
@ -271,10 +277,11 @@ func TestFrontendLoggingEndpoint(t *testing.T) {
},
}
logSentryEventScenario(t, "Should load sourcemap and transform stacktrace line when possible", errorEventForSourceMapping, func(sc *scenarioContext, logs []*log.Record, sourceMapReads []SourceMapReadRecord) {
assert.Equal(t, 200, sc.resp.Code)
assert.Len(t, logs, 1)
assertContextContains(t, logs[0], "stacktrace", `UserError: Please replace user and try again
logSentryEventScenario(t, "Should load sourcemap and transform stacktrace line when possible",
errorEventForSourceMapping, func(sc *scenarioContext, logs []*log.Record, sourceMapReads []SourceMapReadRecord) {
assert.Equal(t, 200, sc.resp.Code)
assert.Len(t, logs, 1)
assertContextContains(t, logs[0], "stacktrace", `UserError: Please replace user and try again
at ? (core|webpack:///./some_source.ts:2:2)
at ? (telepathic|webpack:///./some_source.ts:3:2)
at explode (http://localhost:3000/public/build/error.js:3:10)
@ -282,20 +289,20 @@ func TestFrontendLoggingEndpoint(t *testing.T) {
at nope (http://localhost:3000/baz.js:3:10)
at fake (http://localhost:3000/public/build/../../secrets.txt:3:10)
at ? (core|webpack:///./some_source.ts:3:2)`)
assert.Len(t, sourceMapReads, 6)
assert.Equal(t, "/staticroot", sourceMapReads[0].dir)
assert.Equal(t, "build/moo/foo.js.map", sourceMapReads[0].path)
assert.Equal(t, "/usr/local/telepathic-panel", sourceMapReads[1].dir)
assert.Equal(t, "/foo.js.map", sourceMapReads[1].path)
assert.Equal(t, "/staticroot", sourceMapReads[2].dir)
assert.Equal(t, "build/error.js.map", sourceMapReads[2].path)
assert.Equal(t, "/staticroot", sourceMapReads[3].dir)
assert.Equal(t, "build/bar.js.map", sourceMapReads[3].path)
assert.Equal(t, "/staticroot", sourceMapReads[4].dir)
assert.Equal(t, "secrets.txt.map", sourceMapReads[4].path)
assert.Equal(t, "/staticroot", sourceMapReads[5].dir)
assert.Equal(t, "build/foo.js.map", sourceMapReads[5].path)
})
assert.Len(t, sourceMapReads, 6)
assert.Equal(t, "/staticroot", sourceMapReads[0].dir)
assert.Equal(t, "build/moo/foo.js.map", sourceMapReads[0].path)
assert.Equal(t, "/usr/local/telepathic-panel", sourceMapReads[1].dir)
assert.Equal(t, "/foo.js.map", sourceMapReads[1].path)
assert.Equal(t, "/staticroot", sourceMapReads[2].dir)
assert.Equal(t, "build/error.js.map", sourceMapReads[2].path)
assert.Equal(t, "/staticroot", sourceMapReads[3].dir)
assert.Equal(t, "build/bar.js.map", sourceMapReads[3].path)
assert.Equal(t, "/staticroot", sourceMapReads[4].dir)
assert.Equal(t, "secrets.txt.map", sourceMapReads[4].path)
assert.Equal(t, "/staticroot", sourceMapReads[5].dir)
assert.Equal(t, "build/foo.js.map", sourceMapReads[5].path)
})
})
}

@ -12,7 +12,7 @@ import (
sourcemap "github.com/go-sourcemap/sourcemap"
"github.com/getsentry/sentry-go"
"github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
)
@ -47,12 +47,14 @@ type SourceMapStore struct {
cache map[string]*sourceMap
cfg *setting.Cfg
readSourceMap ReadSourceMapFn
pluginManager plugins.Manager
}
func NewSourceMapStore(cfg *setting.Cfg, readSourceMap ReadSourceMapFn) *SourceMapStore {
func NewSourceMapStore(cfg *setting.Cfg, pluginManager plugins.Manager, readSourceMap ReadSourceMapFn) *SourceMapStore {
return &SourceMapStore{
cache: make(map[string]*sourceMap),
cfg: cfg,
pluginManager: pluginManager,
readSourceMap: readSourceMap,
}
}
@ -69,7 +71,8 @@ func (store *SourceMapStore) guessSourceMapLocation(sourceURL string) (*sourceMa
}
// determine if source comes from grafana core, locally or CDN, look in public build dir on fs
if strings.HasPrefix(u.Path, "/public/build/") || (store.cfg.CDNRootURL != nil && strings.HasPrefix(sourceURL, store.cfg.CDNRootURL.String()) && strings.Contains(u.Path, "/public/build/")) {
if strings.HasPrefix(u.Path, "/public/build/") || (store.cfg.CDNRootURL != nil &&
strings.HasPrefix(sourceURL, store.cfg.CDNRootURL.String()) && strings.Contains(u.Path, "/public/build/")) {
pathParts := strings.SplitN(u.Path, "/public/build/", 2)
if len(pathParts) == 2 {
return &sourceMapLocation{
@ -80,7 +83,7 @@ func (store *SourceMapStore) guessSourceMapLocation(sourceURL string) (*sourceMa
}
// if source comes from a plugin, look in plugin dir
} else if strings.HasPrefix(u.Path, "/public/plugins/") {
for _, route := range manager.StaticRoutes {
for _, route := range store.pluginManager.StaticRoutes() {
pluginPrefix := filepath.Join("/public/plugins/", route.PluginId)
if strings.HasPrefix(u.Path, pluginPrefix) {
return &sourceMapLocation{

@ -32,7 +32,6 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
_ "github.com/grafana/grafana/pkg/plugins/backendplugin/manager"
"github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/plugins/plugindashboards"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/contexthandler"
@ -321,7 +320,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m.Use(middleware.Recovery(hs.Cfg))
for _, route := range manager.StaticRoutes {
for _, route := range hs.PluginManager.StaticRoutes() {
pluginRoute := path.Join("/public/plugins/", route.PluginId)
hs.log.Debug("Plugins: Adding route", "route", pluginRoute, "dir", route.Directory)
hs.mapStatic(m, route.Directory, "", pluginRoute)

@ -16,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/adapters"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/util/errutil"
)
@ -168,7 +167,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon
SignatureOrg: def.SignatureOrg,
}
if app, ok := manager.Apps[def.Id]; ok {
if app := hs.PluginManager.GetApp(def.Id); app != nil {
dto.Enabled = app.AutoEnabled
dto.Pinned = app.AutoEnabled
}
@ -187,16 +186,15 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon
return response.JSON(200, dto)
}
func UpdatePluginSetting(c *models.ReqContext, cmd models.UpdatePluginSettingCmd) response.Response {
func (hs *HTTPServer) UpdatePluginSetting(c *models.ReqContext, cmd models.UpdatePluginSettingCmd) response.Response {
pluginID := c.Params(":pluginId")
cmd.OrgId = c.OrgId
cmd.PluginId = pluginID
if _, ok := manager.Apps[cmd.PluginId]; !ok {
return response.Error(404, "Plugin not installed.", nil)
if app := hs.PluginManager.GetApp(pluginID); app == nil {
return response.Error(404, "Plugin not installed", nil)
}
cmd.OrgId = c.OrgId
cmd.PluginId = pluginID
if err := bus.Dispatch(&cmd); err != nil {
return response.Error(500, "Failed to update plugin setting", err)
}

@ -12,7 +12,6 @@ import (
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins/manager"
)
var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
@ -56,8 +55,8 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport,
metrics["stats.users.count"] = statsQuery.Result.Users
metrics["stats.orgs.count"] = statsQuery.Result.Orgs
metrics["stats.playlist.count"] = statsQuery.Result.Playlists
metrics["stats.plugins.apps.count"] = len(manager.Apps)
metrics["stats.plugins.panels.count"] = len(manager.Panels)
metrics["stats.plugins.apps.count"] = uss.PluginManager.AppCount()
metrics["stats.plugins.panels.count"] = uss.PluginManager.PanelCount()
metrics["stats.plugins.datasources.count"] = uss.PluginManager.DataSourceCount()
metrics["stats.alerts.count"] = statsQuery.Result.Alerts
metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers

@ -294,8 +294,8 @@ func TestMetrics(t *testing.T) {
assert.Equal(t, getSystemStatsQuery.Result.Users, metrics.Get("stats.users.count").MustInt64())
assert.Equal(t, getSystemStatsQuery.Result.Orgs, metrics.Get("stats.orgs.count").MustInt64())
assert.Equal(t, getSystemStatsQuery.Result.Playlists, metrics.Get("stats.playlist.count").MustInt64())
assert.Equal(t, len(manager.Apps), metrics.Get("stats.plugins.apps.count").MustInt())
assert.Equal(t, len(manager.Panels), metrics.Get("stats.plugins.panels.count").MustInt())
assert.Equal(t, uss.PluginManager.AppCount(), metrics.Get("stats.plugins.apps.count").MustInt())
assert.Equal(t, uss.PluginManager.PanelCount(), metrics.Get("stats.plugins.panels.count").MustInt())
assert.Equal(t, uss.PluginManager.DataSourceCount(), metrics.Get("stats.plugins.datasources.count").MustInt())
assert.Equal(t, getSystemStatsQuery.Result.Alerts, metrics.Get("stats.alerts.count").MustInt64())
assert.Equal(t, getSystemStatsQuery.Result.ActiveUsers, metrics.Get("stats.active_users.count").MustInt64())
@ -546,6 +546,7 @@ type fakePluginManager struct {
manager.PluginManager
dataSources map[string]*plugins.DataSourcePlugin
panels map[string]*plugins.PanelPlugin
}
func (pm fakePluginManager) DataSourceCount() int {
@ -556,6 +557,10 @@ func (pm fakePluginManager) GetDataSource(id string) *plugins.DataSourcePlugin {
return pm.dataSources[id]
}
func (pm fakePluginManager) PanelCount() int {
return len(pm.panels)
}
func setupSomeDataSourcePlugins(t *testing.T, uss *UsageStatsService) {
t.Helper()

@ -17,10 +17,19 @@ type Manager interface {
GetDataPlugin(id string) DataPlugin
// GetPlugin gets a plugin with a certain ID.
GetPlugin(id string) *PluginBase
// GetApp gets an app plugin with a certain ID.
GetApp(id string) *AppPlugin
// DataSourceCount gets the number of data sources.
DataSourceCount() int
// DataSources gets all data sources.
DataSources() []*DataSourcePlugin
// Apps gets all app plugins.
Apps() []*AppPlugin
// PanelCount gets the number of panels.
PanelCount() int
// AppCount gets the number of apps.
AppCount() int
// GetEnabledPlugins gets enabled plugins.
// GetEnabledPlugins gets enabled plugins.
GetEnabledPlugins(orgID int64) (*EnabledPlugins, error)
// GrafanaLatestVersion gets the latest Grafana version.
@ -29,6 +38,8 @@ type Manager interface {
GrafanaHasUpdate() bool
// Plugins gets all plugins.
Plugins() []*PluginBase
// StaticRoutes gets all static routes.
StaticRoutes() []*PluginStaticRoute
// GetPluginSettings gets settings for a certain plugin.
GetPluginSettings(orgID int64) (map[string]*models.PluginSettingInfoDTO, error)
// GetPluginDashboards gets dashboards for a certain org/plugin.
@ -41,6 +52,10 @@ type Manager interface {
requestHandler DataRequestHandler) (PluginDashboardInfoDTO, error)
// ScanningErrors returns plugin scanning errors encountered.
ScanningErrors() []PluginError
// LoadPluginDashboard loads a plugin dashboard.
LoadPluginDashboard(pluginID, path string) (*models.Dashboard, error)
// IsAppInstalled returns whether an app is installed.
IsAppInstalled(id string) bool
}
type ImportDashboardInput struct {

@ -78,16 +78,14 @@ func pluginScenario(t *testing.T, desc string, fn func(*testing.T, *PluginManage
t.Helper()
t.Run("Given a plugin", func(t *testing.T) {
pm := &PluginManager{
Cfg: &setting.Cfg{
FeatureToggles: map[string]bool{},
PluginSettings: setting.PluginSettings{
"test-app": map[string]string{
"path": "testdata/test-app",
},
pm := newManager(&setting.Cfg{
FeatureToggles: map[string]bool{},
PluginSettings: setting.PluginSettings{
"test-app": map[string]string{
"path": "testdata/test-app",
},
},
}
})
err := pm.Init()
require.NoError(t, err)

@ -10,16 +10,16 @@ import (
"github.com/grafana/grafana/pkg/plugins"
)
func (pm *PluginManager) GetPluginDashboards(orgId int64, pluginId string) ([]*plugins.PluginDashboardInfoDTO, error) {
plugin, exists := pm.plugins[pluginId]
func (pm *PluginManager) GetPluginDashboards(orgID int64, pluginID string) ([]*plugins.PluginDashboardInfoDTO, error) {
plugin, exists := pm.plugins[pluginID]
if !exists {
return nil, plugins.PluginNotFoundError{PluginID: pluginId}
return nil, plugins.PluginNotFoundError{PluginID: pluginID}
}
result := make([]*plugins.PluginDashboardInfoDTO, 0)
// load current dashboards
query := models.GetDashboardsByPluginIdQuery{OrgId: orgId, PluginId: pluginId}
query := models.GetDashboardsByPluginIdQuery{OrgId: orgID, PluginId: pluginID}
if err := bus.Dispatch(&query); err != nil {
return nil, err
}
@ -70,10 +70,10 @@ func (pm *PluginManager) GetPluginDashboards(orgId int64, pluginId string) ([]*p
return result, nil
}
func (pm *PluginManager) LoadPluginDashboard(pluginId, path string) (*models.Dashboard, error) {
plugin, exists := pm.plugins[pluginId]
func (pm *PluginManager) LoadPluginDashboard(pluginID, path string) (*models.Dashboard, error) {
plugin, exists := pm.plugins[pluginID]
if !exists {
return nil, plugins.PluginNotFoundError{PluginID: pluginId}
return nil, plugins.PluginNotFoundError{PluginID: pluginID}
}
dashboardFilePath := filepath.Join(plugin.PluginDir, path)

@ -7,61 +7,53 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPluginDashboards(t *testing.T) {
Convey("When asking for plugin dashboard info", t, func() {
pm := &PluginManager{
Cfg: &setting.Cfg{
FeatureToggles: map[string]bool{},
PluginSettings: setting.PluginSettings{
"test-app": map[string]string{
"path": "testdata/test-app",
},
},
func TestGetPluginDashboards(t *testing.T) {
pm := newManager(&setting.Cfg{
FeatureToggles: map[string]bool{},
PluginSettings: setting.PluginSettings{
"test-app": map[string]string{
"path": "testdata/test-app",
},
},
})
err := pm.Init()
require.NoError(t, err)
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
if query.Slug == "nginx-connections" {
dash := models.NewDashboard("Nginx Connections")
dash.Data.Set("revision", "1.1")
query.Result = dash
return nil
}
err := pm.Init()
So(err, ShouldBeNil)
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
if query.Slug == "nginx-connections" {
dash := models.NewDashboard("Nginx Connections")
dash.Data.Set("revision", "1.1")
query.Result = dash
return nil
}
return models.ErrDashboardNotFound
})
return models.ErrDashboardNotFound
})
bus.AddHandler("test", func(query *models.GetDashboardsByPluginIdQuery) error {
var data = simplejson.New()
data.Set("title", "Nginx Connections")
data.Set("revision", 22)
bus.AddHandler("test", func(query *models.GetDashboardsByPluginIdQuery) error {
var data = simplejson.New()
data.Set("title", "Nginx Connections")
data.Set("revision", 22)
query.Result = []*models.Dashboard{
{Slug: "nginx-connections", Data: data},
}
return nil
})
dashboards, err := pm.GetPluginDashboards(1, "test-app")
So(err, ShouldBeNil)
query.Result = []*models.Dashboard{
{Slug: "nginx-connections", Data: data},
}
return nil
})
Convey("should return 2 dashboards", func() {
So(len(dashboards), ShouldEqual, 2)
})
dashboards, err := pm.GetPluginDashboards(1, "test-app")
require.NoError(t, err)
Convey("should include installed version info", func() {
So(dashboards[0].Title, ShouldEqual, "Nginx Connections")
So(dashboards[0].Revision, ShouldEqual, 25)
So(dashboards[0].ImportedRevision, ShouldEqual, 22)
So(dashboards[0].ImportedUri, ShouldEqual, "db/nginx-connections")
assert.Len(t, dashboards, 2)
assert.Equal(t, "Nginx Connections", dashboards[0].Title)
assert.Equal(t, int64(25), dashboards[0].Revision)
assert.Equal(t, int64(22), dashboards[0].ImportedRevision)
assert.Equal(t, "db/nginx-connections", dashboards[0].ImportedUri)
So(dashboards[1].Revision, ShouldEqual, 2)
So(dashboards[1].ImportedRevision, ShouldEqual, 0)
})
})
assert.Equal(t, int64(2), dashboards[1].Revision)
assert.Equal(t, int64(0), dashboards[1].ImportedRevision)
}

@ -28,11 +28,6 @@ import (
)
var (
Panels map[string]*plugins.PanelPlugin
StaticRoutes []*plugins.PluginStaticRoute
Apps map[string]*plugins.AppPlugin
PluginTypes map[string]interface{}
plog log.Logger
)
@ -63,31 +58,34 @@ type PluginManager struct {
grafanaHasUpdate bool
pluginScanningErrors map[string]plugins.PluginError
renderer *plugins.RendererPlugin
dataSources map[string]*plugins.DataSourcePlugin
plugins map[string]*plugins.PluginBase
renderer *plugins.RendererPlugin
dataSources map[string]*plugins.DataSourcePlugin
plugins map[string]*plugins.PluginBase
panels map[string]*plugins.PanelPlugin
apps map[string]*plugins.AppPlugin
staticRoutes []*plugins.PluginStaticRoute
}
func init() {
registry.RegisterService(&PluginManager{
dataSources: map[string]*plugins.DataSourcePlugin{},
registry.Register(&registry.Descriptor{
Name: "PluginManager",
Instance: newManager(nil),
})
}
func newManager(cfg *setting.Cfg) *PluginManager {
return &PluginManager{
Cfg: cfg,
dataSources: map[string]*plugins.DataSourcePlugin{},
plugins: map[string]*plugins.PluginBase{},
panels: map[string]*plugins.PanelPlugin{},
apps: map[string]*plugins.AppPlugin{},
}
}
func (pm *PluginManager) Init() error {
pm.log = log.New("plugins")
plog = log.New("plugins")
StaticRoutes = []*plugins.PluginStaticRoute{}
Panels = map[string]*plugins.PanelPlugin{}
Apps = map[string]*plugins.AppPlugin{}
pm.plugins = map[string]*plugins.PluginBase{}
PluginTypes = map[string]interface{}{
"panel": plugins.PanelPlugin{},
"datasource": plugins.DataSourcePlugin{},
"app": plugins.AppPlugin{},
"renderer": plugins.RendererPlugin{},
}
pm.pluginScanningErrors = map[string]plugins.PluginError{}
pm.log.Info("Starting plugin search")
@ -133,24 +131,24 @@ func (pm *PluginManager) Init() error {
return err
}
for _, panel := range Panels {
for _, panel := range pm.panels {
staticRoutes := panel.InitFrontendPlugin(pm.Cfg)
StaticRoutes = append(StaticRoutes, staticRoutes...)
pm.staticRoutes = append(pm.staticRoutes, staticRoutes...)
}
for _, ds := range pm.dataSources {
staticRoutes := ds.InitFrontendPlugin(pm.Cfg)
StaticRoutes = append(StaticRoutes, staticRoutes...)
pm.staticRoutes = append(pm.staticRoutes, staticRoutes...)
}
for _, app := range Apps {
staticRoutes := app.InitApp(Panels, pm.dataSources, pm.Cfg)
StaticRoutes = append(StaticRoutes, staticRoutes...)
for _, app := range pm.apps {
staticRoutes := app.InitApp(pm.panels, pm.dataSources, pm.Cfg)
pm.staticRoutes = append(pm.staticRoutes, staticRoutes...)
}
if pm.renderer != nil {
staticRoutes := pm.renderer.InitFrontendPlugin(pm.Cfg)
StaticRoutes = append(StaticRoutes, staticRoutes...)
pm.staticRoutes = append(pm.staticRoutes, staticRoutes...)
}
for _, p := range pm.plugins {
@ -203,6 +201,14 @@ func (pm *PluginManager) DataSourceCount() int {
return len(pm.dataSources)
}
func (pm *PluginManager) PanelCount() int {
return len(pm.panels)
}
func (pm *PluginManager) AppCount() int {
return len(pm.apps)
}
func (pm *PluginManager) Plugins() []*plugins.PluginBase {
var rslt []*plugins.PluginBase
for _, p := range pm.plugins {
@ -212,10 +218,23 @@ func (pm *PluginManager) Plugins() []*plugins.PluginBase {
return rslt
}
func (pm *PluginManager) Apps() []*plugins.AppPlugin {
var rslt []*plugins.AppPlugin
for _, p := range pm.apps {
rslt = append(rslt, p)
}
return rslt
}
func (pm *PluginManager) GetPlugin(id string) *plugins.PluginBase {
return pm.plugins[id]
}
func (pm *PluginManager) GetApp(id string) *plugins.AppPlugin {
return pm.apps[id]
}
func (pm *PluginManager) GrafanaLatestVersion() string {
return pm.grafanaLatestVersion
}
@ -270,6 +289,13 @@ func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error {
pm.log.Debug("Initial plugin loading done")
pluginTypes := map[string]interface{}{
"panel": plugins.PanelPlugin{},
"datasource": plugins.DataSourcePlugin{},
"app": plugins.AppPlugin{},
"renderer": plugins.RendererPlugin{},
}
// 2nd pass: Validate and register plugins
for dpath, plugin := range scanner.plugins {
// Try to find any root plugin
@ -298,7 +324,7 @@ func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error {
pm.log.Debug("Attempting to add plugin", "id", plugin.Id)
pluginGoType, exists := PluginTypes[plugin.Type]
pluginGoType, exists := pluginTypes[plugin.Type]
if !exists {
return fmt.Errorf("unknown plugin type %q", plugin.Type)
}
@ -364,13 +390,13 @@ func (pm *PluginManager) loadPlugin(jsonParser *json.Decoder, pluginBase *plugin
pm.dataSources[p.Id] = p
pb = &p.PluginBase
case *plugins.PanelPlugin:
Panels[p.Id] = p
pm.panels[p.Id] = p
pb = &p.PluginBase
case *plugins.RendererPlugin:
pm.renderer = p
pb = &p.PluginBase
case *plugins.AppPlugin:
Apps[p.Id] = p
pm.apps[p.Id] = p
pb = &p.PluginBase
default:
panic(fmt.Sprintf("Unrecognized plugin type %T", plug))
@ -651,3 +677,7 @@ func (pm *PluginManager) GetDataPlugin(id string) plugins.DataPlugin {
return nil
}
func (pm *PluginManager) StaticRoutes() []*plugins.PluginStaticRoute {
return pm.staticRoutes
}

@ -31,11 +31,11 @@ func TestPluginManager_Init(t *testing.T) {
assert.Empty(t, pm.scanningErrors)
assert.Greater(t, len(pm.dataSources), 1)
assert.Greater(t, len(Panels), 1)
assert.Greater(t, len(pm.panels), 1)
assert.Equal(t, "app/plugins/datasource/graphite/module", pm.dataSources["graphite"].Module)
assert.NotEmpty(t, Apps)
assert.Equal(t, "public/plugins/test-app/img/logo_large.png", Apps["test-app"].Info.Logos.Large)
assert.Equal(t, "public/plugins/test-app/img/screenshot2.png", Apps["test-app"].Info.Screenshots[1].Path)
assert.NotEmpty(t, pm.apps)
assert.Equal(t, "public/plugins/test-app/img/logo_large.png", pm.apps["test-app"].Info.Logos.Large)
assert.Equal(t, "public/plugins/test-app/img/screenshot2.png", pm.apps["test-app"].Info.Screenshots[1].Path)
})
t.Run("With external back-end plugin lacking signature", func(t *testing.T) {
@ -253,15 +253,12 @@ func createManager(t *testing.T, cbs ...func(*PluginManager)) *PluginManager {
staticRootPath, err := filepath.Abs("../../../public/")
require.NoError(t, err)
pm := &PluginManager{
Cfg: &setting.Cfg{
Raw: ini.Empty(),
Env: setting.Prod,
StaticRootPath: staticRootPath,
},
BackendPluginManager: &fakeBackendPluginManager{},
dataSources: map[string]*plugins.DataSourcePlugin{},
}
pm := newManager(&setting.Cfg{
Raw: ini.Empty(),
Env: setting.Prod,
StaticRootPath: staticRootPath,
})
pm.BackendPluginManager = &fakeBackendPluginManager{}
for _, cb := range cbs {
cb(pm)
}

@ -30,7 +30,7 @@ func (pm *PluginManager) GetPluginSettings(orgID int64) (map[string]*models.Plug
}
// apps are disabled by default unless autoEnabled: true
if app, exists := Apps[pluginDef.Id]; exists {
if app, exists := pm.apps[pluginDef.Id]; exists {
opt.Enabled = app.AutoEnabled
opt.Pinned = app.AutoEnabled
}
@ -63,7 +63,7 @@ func (pm *PluginManager) GetEnabledPlugins(orgID int64) (*plugins.EnabledPlugins
return enabledPlugins, err
}
for pluginID, app := range Apps {
for pluginID, app := range pm.apps {
if b, ok := pluginSettingMap[pluginID]; ok {
app.Pinned = b.Pinned
enabledPlugins.Apps = append(enabledPlugins.Apps, app)
@ -77,7 +77,7 @@ func (pm *PluginManager) GetEnabledPlugins(orgID int64) (*plugins.EnabledPlugins
}
}
for _, panel := range Panels {
for _, panel := range pm.panels {
if _, exists := pluginSettingMap[panel.Id]; exists {
enabledPlugins.Panels = append(enabledPlugins.Panels, panel)
}
@ -87,7 +87,7 @@ func (pm *PluginManager) GetEnabledPlugins(orgID int64) (*plugins.EnabledPlugins
}
// IsAppInstalled checks if an app plugin with provided plugin ID is installed.
func IsAppInstalled(pluginID string) bool {
_, exists := Apps[pluginID]
func (pm *PluginManager) IsAppInstalled(pluginID string) bool {
_, exists := pm.apps[pluginID]
return exists
}

@ -7,7 +7,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/tsdb"
@ -21,9 +20,9 @@ func init() {
}
type Service struct {
DataService *tsdb.Service `inject:""`
PluginManager *manager.PluginManager `inject:""`
SQLStore *sqlstore.SQLStore `inject:""`
DataService *tsdb.Service `inject:""`
PluginManager plugins.Manager `inject:""`
SQLStore *sqlstore.SQLStore `inject:""`
logger log.Logger
}

@ -29,7 +29,7 @@ import (
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/middleware"
_ "github.com/grafana/grafana/pkg/plugins"
_ "github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/registry"
_ "github.com/grafana/grafana/pkg/services/alerting"
_ "github.com/grafana/grafana/pkg/services/auth"

@ -8,7 +8,7 @@ import (
"strings"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins/manager"
"github.com/grafana/grafana/pkg/plugins"
"gopkg.in/yaml.v2"
)
@ -17,11 +17,12 @@ type configReader interface {
}
type configReaderImpl struct {
log log.Logger
log log.Logger
pluginManager plugins.Manager
}
func newConfigReader(logger log.Logger) configReader {
return &configReaderImpl{log: logger}
func newConfigReader(logger log.Logger, pluginManager plugins.Manager) configReader {
return &configReaderImpl{log: logger, pluginManager: pluginManager}
}
func (cr *configReaderImpl) readConfig(path string) ([]*pluginsAsConfig, error) {
@ -112,8 +113,8 @@ func (cr *configReaderImpl) validatePluginsConfig(apps []*pluginsAsConfig) error
}
for _, app := range apps[i].Apps {
if !manager.IsAppInstalled(app.PluginID) {
return fmt.Errorf("app plugin not installed: %s", app.PluginID)
if !cr.pluginManager.IsAppInstalled(app.PluginID) {
return fmt.Errorf("app plugin not installed: %q", app.PluginID)
}
}
}

@ -6,7 +6,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager"
"github.com/stretchr/testify/require"
)
@ -20,36 +19,38 @@ const (
func TestConfigReader(t *testing.T) {
t.Run("Broken yaml should return error", func(t *testing.T) {
reader := newConfigReader(log.New("test logger"))
reader := newConfigReader(log.New("test logger"), nil)
_, err := reader.readConfig(brokenYaml)
require.Error(t, err)
})
t.Run("Skip invalid directory", func(t *testing.T) {
cfgProvider := newConfigReader(log.New("test logger"))
cfgProvider := newConfigReader(log.New("test logger"), nil)
cfg, err := cfgProvider.readConfig(emptyFolder)
require.NoError(t, err)
require.Len(t, cfg, 0)
})
t.Run("Unknown app plugin should return error", func(t *testing.T) {
cfgProvider := newConfigReader(log.New("test logger"))
cfgProvider := newConfigReader(log.New("test logger"), fakePluginManager{})
_, err := cfgProvider.readConfig(unknownApp)
require.Error(t, err)
require.Equal(t, "app plugin not installed: nonexisting", err.Error())
require.Equal(t, "app plugin not installed: \"nonexisting\"", err.Error())
})
t.Run("Read incorrect properties", func(t *testing.T) {
cfgProvider := newConfigReader(log.New("test logger"))
cfgProvider := newConfigReader(log.New("test logger"), nil)
_, err := cfgProvider.readConfig(incorrectSettings)
require.Error(t, err)
require.Equal(t, "app item 1 in configuration doesn't contain required field type", err.Error())
})
t.Run("Can read correct properties", func(t *testing.T) {
manager.Apps = map[string]*plugins.AppPlugin{
"test-plugin": {},
"test-plugin-2": {},
pm := fakePluginManager{
apps: map[string]*plugins.AppPlugin{
"test-plugin": {},
"test-plugin-2": {},
},
}
err := os.Setenv("ENABLE_PLUGIN_VAR", "test-plugin")
@ -58,7 +59,7 @@ func TestConfigReader(t *testing.T) {
_ = os.Unsetenv("ENABLE_PLUGIN_VAR")
})
cfgProvider := newConfigReader(log.New("test logger"))
cfgProvider := newConfigReader(log.New("test logger"), pm)
cfg, err := cfgProvider.readConfig(correctProperties)
require.NoError(t, err)
require.Len(t, cfg, 1)
@ -85,3 +86,14 @@ func TestConfigReader(t *testing.T) {
}
})
}
type fakePluginManager struct {
plugins.Manager
apps map[string]*plugins.AppPlugin
}
func (pm fakePluginManager) IsAppInstalled(id string) bool {
_, exists := pm.apps[id]
return exists
}

@ -6,12 +6,17 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
)
// Provision scans a directory for provisioning config files
// and provisions the app in those files.
func Provision(configDirectory string) error {
ap := newAppProvisioner(log.New("provisioning.plugins"))
func Provision(configDirectory string, pluginManager plugins.Manager) error {
logger := log.New("provisioning.plugins")
ap := PluginProvisioner{
log: logger,
cfgProvider: newConfigReader(logger, pluginManager),
}
return ap.applyChanges(configDirectory)
}
@ -22,13 +27,6 @@ type PluginProvisioner struct {
cfgProvider configReader
}
func newAppProvisioner(log log.Logger) PluginProvisioner {
return PluginProvisioner{
log: log,
cfgProvider: newConfigReader(log),
}
}
func (ap *PluginProvisioner) apply(cfg *pluginsAsConfig) error {
for _, app := range cfg.Apps {
if app.OrgID == 0 && app.OrgName != "" {

@ -43,7 +43,7 @@ func newProvisioningServiceImpl(
newDashboardProvisioner dashboards.DashboardProvisionerFactory,
provisionNotifiers func(string) error,
provisionDatasources func(string) error,
provisionPlugins func(string) error,
provisionPlugins func(string, plugifaces.Manager) error,
) *provisioningServiceImpl {
return &provisioningServiceImpl{
log: log.New("provisioning"),
@ -58,13 +58,14 @@ type provisioningServiceImpl struct {
Cfg *setting.Cfg `inject:""`
RequestHandler plugifaces.DataRequestHandler `inject:""`
SQLStore *sqlstore.SQLStore `inject:""`
PluginManager plugifaces.Manager `inject:""`
log log.Logger
pollingCtxCancel context.CancelFunc
newDashboardProvisioner dashboards.DashboardProvisionerFactory
dashboardProvisioner dashboards.DashboardProvisioner
provisionNotifiers func(string) error
provisionDatasources func(string) error
provisionPlugins func(string) error
provisionPlugins func(string, plugifaces.Manager) error
mutex sync.Mutex
}
@ -124,7 +125,7 @@ func (ps *provisioningServiceImpl) ProvisionDatasources() error {
func (ps *provisioningServiceImpl) ProvisionPlugins() error {
appPath := filepath.Join(ps.Cfg.ProvisioningPath, "plugins")
err := ps.provisionPlugins(appPath)
err := ps.provisionPlugins(appPath, ps.PluginManager)
return errutil.Wrap("app provisioning error", err)
}

Loading…
Cancel
Save