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

* toying around

* fix refs

* remove unused fields

* go further

* add context

* ensure streaming handler is set
pull/41800/head
Will Browne 4 years ago committed by GitHub
parent dbb8246b6b
commit 2e3e7a7e55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      pkg/api/app_routes.go
  2. 10
      pkg/api/dashboard.go
  3. 10
      pkg/api/datasources.go
  4. 8
      pkg/api/dtos/plugins.go
  5. 27
      pkg/api/fakes.go
  6. 26
      pkg/api/frontendsettings.go
  7. 81
      pkg/api/plugins.go
  8. 36
      pkg/api/plugins_test.go
  9. 10
      pkg/infra/usagestats/service/usage_stats.go
  10. 14
      pkg/infra/usagestats/service/usage_stats_test.go
  11. 4
      pkg/plugins/ifaces.go
  12. 8
      pkg/plugins/manager/dashboards.go
  13. 207
      pkg/plugins/manager/manager.go
  14. 47
      pkg/plugins/manager/manager_integration_test.go
  15. 66
      pkg/plugins/manager/manager_test.go
  16. 120
      pkg/plugins/manager/store.go
  17. 13
      pkg/plugins/manager/update_checker.go
  18. 4
      pkg/plugins/plugincontext/plugincontext.go
  19. 26
      pkg/plugins/plugindashboards/service.go
  20. 109
      pkg/plugins/plugins.go
  21. 4
      pkg/services/datasourceproxy/datasourceproxy.go
  22. 4
      pkg/services/live/live.go
  23. 3
      pkg/services/provisioning/plugins/config_reader.go
  24. 11
      pkg/services/provisioning/plugins/config_reader_test.go

@ -1,6 +1,7 @@
package api
import (
"context"
"crypto/tls"
"net"
"net/http"
@ -33,7 +34,7 @@ func (hs *HTTPServer) initAppPluginRoutes(r *web.Mux) {
TLSHandshakeTimeout: 10 * time.Second,
}
for _, plugin := range hs.pluginStore.Plugins(plugins.App) {
for _, plugin := range hs.pluginStore.Plugins(context.TODO(), plugins.App) {
for _, route := range plugin.Routes {
url := util.JoinURLFragments("/api/plugin-proxy/"+plugin.ID, route.Path)
handlers := make([]web.Handler, 0)

@ -359,7 +359,7 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa
}
if err != nil {
return hs.dashboardSaveErrorToApiResponse(err)
return hs.dashboardSaveErrorToApiResponse(ctx, err)
}
if hs.Cfg.EditorsCanAdmin && newDashboard {
@ -371,7 +371,7 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa
}
// connect library panels for this dashboard after the dashboard is stored and has an ID
err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(c.Req.Context(), c.SignedInUser, dashboard)
err = hs.LibraryPanelService.ConnectLibraryPanelsForDashboard(ctx, c.SignedInUser, dashboard)
if err != nil {
return response.Error(500, "Error while connecting library panels", err)
}
@ -387,7 +387,7 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext, cmd models.SaveDashboa
})
}
func (hs *HTTPServer) dashboardSaveErrorToApiResponse(err error) response.Response {
func (hs *HTTPServer) dashboardSaveErrorToApiResponse(ctx context.Context, err error) response.Response {
var dashboardErr models.DashboardErr
if ok := errors.As(err, &dashboardErr); ok {
if body := dashboardErr.Body(); body != nil {
@ -412,8 +412,8 @@ func (hs *HTTPServer) dashboardSaveErrorToApiResponse(err error) response.Respon
if ok := errors.As(err, &pluginErr); ok {
message := fmt.Sprintf("The dashboard belongs to plugin %s.", pluginErr.PluginId)
// look up plugin name
if pluginDef := hs.pluginStore.Plugin(pluginErr.PluginId); pluginDef != nil {
message = fmt.Sprintf("The dashboard belongs to plugin %s.", pluginDef.Name)
if plugin, exists := hs.pluginStore.Plugin(ctx, pluginErr.PluginId); exists {
message = fmt.Sprintf("The dashboard belongs to plugin %s.", plugin.Name)
}
return response.JSON(412, util.DynMap{"status": "plugin-dashboard", "message": message})
}

@ -48,7 +48,7 @@ func (hs *HTTPServer) GetDataSources(c *models.ReqContext) response.Response {
ReadOnly: ds.ReadOnly,
}
if plugin := hs.pluginStore.Plugin(ds.Type); plugin != nil {
if plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), ds.Type); exists {
dsItem.TypeLogoUrl = plugin.Info.Logos.Small
dsItem.TypeName = plugin.Name
} else {
@ -380,8 +380,8 @@ func (hs *HTTPServer) CallDatasourceResource(c *models.ReqContext) {
return
}
plugin := hs.pluginStore.Plugin(ds.Type)
if plugin == nil {
plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), ds.Type)
if !exists {
c.JsonApiErr(500, "Unable to find datasource plugin", err)
return
}
@ -445,8 +445,8 @@ func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) response.Respo
return response.Error(500, "Unable to load datasource metadata", err)
}
plugin := hs.pluginStore.Plugin(ds.Type)
if plugin == nil {
plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), ds.Type)
if !exists {
return response.Error(500, "Unable to find datasource plugin", err)
}

@ -13,9 +13,9 @@ type PluginSetting struct {
Pinned bool `json:"pinned"`
Module string `json:"module"`
BaseUrl string `json:"baseUrl"`
Info *plugins.Info `json:"info"`
Info plugins.Info `json:"info"`
Includes []*plugins.Includes `json:"includes"`
Dependencies *plugins.Dependencies `json:"dependencies"`
Dependencies plugins.Dependencies `json:"dependencies"`
JsonData map[string]interface{} `json:"jsonData"`
DefaultNavUrl string `json:"defaultNavUrl"`
@ -33,8 +33,8 @@ type PluginListItem struct {
Id string `json:"id"`
Enabled bool `json:"enabled"`
Pinned bool `json:"pinned"`
Info *plugins.Info `json:"info"`
Dependencies *plugins.Dependencies `json:"dependencies"`
Info plugins.Info `json:"info"`
Dependencies plugins.Dependencies `json:"dependencies"`
LatestVersion string `json:"latestVersion"`
HasUpdate bool `json:"hasUpdate"`
DefaultNavUrl string `json:"defaultNavUrl"`

@ -1,17 +1,34 @@
package api
import "github.com/grafana/grafana/pkg/plugins"
import (
"context"
"github.com/grafana/grafana/pkg/plugins"
)
type fakePluginStore struct {
plugins.Store
plugins map[string]plugins.PluginDTO
}
func (ps *fakePluginStore) Plugin(pluginID string) *plugins.Plugin {
return nil
func (pr fakePluginStore) Plugin(_ context.Context, pluginID string) (plugins.PluginDTO, bool) {
p, exists := pr.plugins[pluginID]
return p, exists
}
func (ps *fakePluginStore) Plugins(pluginType ...plugins.Type) []*plugins.Plugin {
return nil
func (pr fakePluginStore) Plugins(_ context.Context, pluginTypes ...plugins.Type) []plugins.PluginDTO {
var result []plugins.PluginDTO
for _, v := range pr.plugins {
for _, t := range pluginTypes {
if v.Type == t {
result = append(result, v)
}
}
}
return result
}
type fakeRendererManager struct {

@ -125,7 +125,7 @@ func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins Enab
// add data sources that are built in (meaning they are not added via data sources page, nor have any entry in
// the datasource table)
for _, ds := range hs.pluginStore.Plugins(plugins.DataSource) {
for _, ds := range hs.pluginStore.Plugins(c.Req.Context(), plugins.DataSource) {
if ds.BuiltIn {
info := map[string]interface{}{
"type": ds.Type,
@ -364,15 +364,15 @@ func (hs *HTTPServer) GetFrontendSettings(c *models.ReqContext) {
}
// EnabledPlugins represents a mapping from plugin types (panel, data source, etc.) to plugin IDs to plugins
// For example ["panel"] -> ["piechart"] -> {pie chart plugin instance}
type EnabledPlugins map[plugins.Type]map[string]*plugins.Plugin
// For example ["panel"] -> ["piechart"] -> {pie chart plugin DTO}
type EnabledPlugins map[plugins.Type]map[string]plugins.PluginDTO
func (ep EnabledPlugins) Get(pluginType plugins.Type, pluginID string) (*plugins.Plugin, bool) {
func (ep EnabledPlugins) Get(pluginType plugins.Type, pluginID string) (plugins.PluginDTO, bool) {
if _, exists := ep[pluginType][pluginID]; exists {
return ep[pluginType][pluginID], true
}
return nil, false
return plugins.PluginDTO{}, false
}
func (hs *HTTPServer) enabledPlugins(ctx context.Context, orgID int64) (EnabledPlugins, error) {
@ -383,8 +383,8 @@ func (hs *HTTPServer) enabledPlugins(ctx context.Context, orgID int64) (EnabledP
return ep, err
}
apps := make(map[string]*plugins.Plugin)
for _, app := range hs.pluginStore.Plugins(plugins.App) {
apps := make(map[string]plugins.PluginDTO)
for _, app := range hs.pluginStore.Plugins(ctx, plugins.App) {
if b, exists := pluginSettingMap[app.ID]; exists {
app.Pinned = b.Pinned
apps[app.ID] = app
@ -392,16 +392,16 @@ func (hs *HTTPServer) enabledPlugins(ctx context.Context, orgID int64) (EnabledP
}
ep[plugins.App] = apps
dataSources := make(map[string]*plugins.Plugin)
for _, ds := range hs.pluginStore.Plugins(plugins.DataSource) {
dataSources := make(map[string]plugins.PluginDTO)
for _, ds := range hs.pluginStore.Plugins(ctx, plugins.DataSource) {
if _, exists := pluginSettingMap[ds.ID]; exists {
dataSources[ds.ID] = ds
}
}
ep[plugins.DataSource] = dataSources
panels := make(map[string]*plugins.Plugin)
for _, p := range hs.pluginStore.Plugins(plugins.Panel) {
panels := make(map[string]plugins.PluginDTO)
for _, p := range hs.pluginStore.Plugins(ctx, plugins.Panel) {
if _, exists := pluginSettingMap[p.ID]; exists {
panels[p.ID] = p
}
@ -424,7 +424,7 @@ func (hs *HTTPServer) pluginSettings(ctx context.Context, orgID int64) (map[stri
}
// fill settings from app plugins
for _, plugin := range hs.pluginStore.Plugins(plugins.App) {
for _, plugin := range hs.pluginStore.Plugins(ctx, plugins.App) {
// ignore settings that already exist
if _, exists := pluginSettings[plugin.ID]; exists {
continue
@ -442,7 +442,7 @@ func (hs *HTTPServer) pluginSettings(ctx context.Context, orgID int64) (map[stri
}
// fill settings from all remaining plugins (including potential app child plugins)
for _, plugin := range hs.pluginStore.Plugins() {
for _, plugin := range hs.pluginStore.Plugins(ctx) {
// ignore settings that already exist
if _, exists := pluginSettings[plugin.ID]; exists {
continue

@ -1,6 +1,7 @@
package api
import (
"context"
"encoding/json"
"errors"
"fmt"
@ -41,7 +42,7 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
}
result := make(dtos.PluginList, 0)
for _, pluginDef := range hs.pluginStore.Plugins() {
for _, pluginDef := range hs.pluginStore.Plugins(c.Req.Context()) {
// filter out app sub plugins
if embeddedFilter == "0" && pluginDef.IncludedInAppID != "" {
continue
@ -66,8 +67,8 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
Name: pluginDef.Name,
Type: string(pluginDef.Type),
Category: pluginDef.Category,
Info: &pluginDef.Info,
Dependencies: &pluginDef.Dependencies,
Info: pluginDef.Info,
Dependencies: pluginDef.Dependencies,
LatestVersion: pluginDef.GrafanaComVersion,
HasUpdate: pluginDef.GrafanaComHasUpdate,
DefaultNavUrl: pluginDef.DefaultNavURL,
@ -106,32 +107,32 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Response {
pluginID := web.Params(c.Req)[":pluginId"]
def := hs.pluginStore.Plugin(pluginID)
if def == nil {
plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
if !exists {
return response.Error(404, "Plugin not found, no installed plugin with that id", nil)
}
dto := &dtos.PluginSetting{
Type: string(def.Type),
Id: def.ID,
Name: def.Name,
Info: &def.Info,
Dependencies: &def.Dependencies,
Includes: def.Includes,
BaseUrl: def.BaseURL,
Module: def.Module,
DefaultNavUrl: def.DefaultNavURL,
LatestVersion: def.GrafanaComVersion,
HasUpdate: def.GrafanaComHasUpdate,
State: def.State,
Signature: def.Signature,
SignatureType: def.SignatureType,
SignatureOrg: def.SignatureOrg,
}
if def.IsApp() {
dto.Enabled = def.AutoEnabled
dto.Pinned = def.AutoEnabled
Type: string(plugin.Type),
Id: plugin.ID,
Name: plugin.Name,
Info: plugin.Info,
Dependencies: plugin.Dependencies,
Includes: plugin.Includes,
BaseUrl: plugin.BaseURL,
Module: plugin.Module,
DefaultNavUrl: plugin.DefaultNavURL,
LatestVersion: plugin.GrafanaComVersion,
HasUpdate: plugin.GrafanaComHasUpdate,
State: plugin.State,
Signature: plugin.Signature,
SignatureType: plugin.SignatureType,
SignatureOrg: plugin.SignatureOrg,
}
if plugin.IsApp() {
dto.Enabled = plugin.AutoEnabled
dto.Pinned = plugin.AutoEnabled
}
query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}
@ -151,7 +152,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon
func (hs *HTTPServer) UpdatePluginSetting(c *models.ReqContext, cmd models.UpdatePluginSettingCmd) response.Response {
pluginID := web.Params(c.Req)[":pluginId"]
if app := hs.pluginStore.Plugin(pluginID); app == nil {
if _, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID); !exists {
return response.Error(404, "Plugin not installed", nil)
}
@ -184,7 +185,7 @@ func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response
pluginID := web.Params(c.Req)[":pluginId"]
name := web.Params(c.Req)[":name"]
content, err := hs.pluginMarkdown(pluginID, name)
content, err := hs.pluginMarkdown(c.Req.Context(), pluginID, name)
if err != nil {
var notFound plugins.NotFoundError
if errors.As(err, &notFound) {
@ -196,7 +197,7 @@ func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response
// fallback try readme
if len(content) == 0 {
content, err = hs.pluginMarkdown(pluginID, "readme")
content, err = hs.pluginMarkdown(c.Req.Context(), pluginID, "readme")
if err != nil {
return response.Error(501, "Could not get markdown file", err)
}
@ -232,7 +233,7 @@ func (hs *HTTPServer) ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDa
dashInfo, dash, err := hs.pluginDashboardManager.ImportDashboard(c.Req.Context(), apiCmd.PluginId, apiCmd.Path, c.OrgId, apiCmd.FolderId,
apiCmd.Dashboard, apiCmd.Overwrite, apiCmd.Inputs, c.SignedInUser)
if err != nil {
return hs.dashboardSaveErrorToApiResponse(err)
return hs.dashboardSaveErrorToApiResponse(c.Req.Context(), err)
}
err = hs.LibraryPanelService.ImportLibraryPanelsForDashboard(c.Req.Context(), c.SignedInUser, dash, apiCmd.FolderId)
@ -253,8 +254,8 @@ func (hs *HTTPServer) ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDa
// /api/plugins/:pluginId/metrics
func (hs *HTTPServer) CollectPluginMetrics(c *models.ReqContext) response.Response {
pluginID := web.Params(c.Req)[":pluginId"]
plugin := hs.pluginStore.Plugin(pluginID)
if plugin == nil {
plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
if !exists {
return response.Error(404, "Plugin not found", nil)
}
@ -274,8 +275,8 @@ func (hs *HTTPServer) CollectPluginMetrics(c *models.ReqContext) response.Respon
// /public/plugins/:pluginId/*
func (hs *HTTPServer) getPluginAssets(c *models.ReqContext) {
pluginID := web.Params(c.Req)[":pluginId"]
plugin := hs.pluginStore.Plugin(pluginID)
if plugin == nil {
plugin, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
if !exists {
c.JsonApiErr(404, "Plugin not found", nil)
return
}
@ -457,22 +458,22 @@ func translatePluginRequestErrorToAPIError(err error) response.Response {
return response.Error(500, "Plugin request failed", err)
}
func (hs *HTTPServer) pluginMarkdown(pluginId string, name string) ([]byte, error) {
plug := hs.pluginStore.Plugin(pluginId)
if plug == nil {
func (hs *HTTPServer) pluginMarkdown(ctx context.Context, pluginId string, name string) ([]byte, error) {
plugin, exists := hs.pluginStore.Plugin(ctx, pluginId)
if !exists {
return nil, plugins.NotFoundError{PluginID: pluginId}
}
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based
// We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based
// on plugin the folder structure on disk and not user input.
path := filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
path := filepath.Join(plugin.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
exists, err := fs.Exists(path)
if err != nil {
return nil, err
}
if !exists {
path = filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name)))
path = filepath.Join(plugin.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name)))
}
exists, err = fs.Exists(path)
@ -484,7 +485,7 @@ func (hs *HTTPServer) pluginMarkdown(pluginId string, name string) ([]byte, erro
}
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based
// We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based
// on plugin the folder structure on disk and not user input.
data, err := ioutil.ReadFile(path)
if err != nil {

@ -34,7 +34,7 @@ func Test_GetPluginAssets(t *testing.T) {
requestedFile := filepath.Clean(tmpFile.Name())
t.Run("Given a request for an existing plugin file that is listed as a signature covered file", func(t *testing.T) {
p := &plugins.Plugin{
p := plugins.PluginDTO{
JSONData: plugins.JSONData{
ID: pluginID,
},
@ -43,8 +43,8 @@ func Test_GetPluginAssets(t *testing.T) {
requestedFile: {},
},
}
service := &pluginStore{
plugins: map[string]*plugins.Plugin{
service := &fakePluginStore{
plugins: map[string]plugins.PluginDTO{
pluginID: p,
},
}
@ -62,14 +62,14 @@ func Test_GetPluginAssets(t *testing.T) {
})
t.Run("Given a request for an existing plugin file that is not listed as a signature covered file", func(t *testing.T) {
p := &plugins.Plugin{
p := plugins.PluginDTO{
JSONData: plugins.JSONData{
ID: pluginID,
},
PluginDir: pluginDir,
}
service := &pluginStore{
plugins: map[string]*plugins.Plugin{
service := &fakePluginStore{
plugins: map[string]plugins.PluginDTO{
pluginID: p,
},
}
@ -87,14 +87,14 @@ func Test_GetPluginAssets(t *testing.T) {
})
t.Run("Given a request for an non-existing plugin file", func(t *testing.T) {
p := &plugins.Plugin{
p := plugins.PluginDTO{
JSONData: plugins.JSONData{
ID: pluginID,
},
PluginDir: pluginDir,
}
service := &pluginStore{
plugins: map[string]*plugins.Plugin{
service := &fakePluginStore{
plugins: map[string]plugins.PluginDTO{
pluginID: p,
},
}
@ -116,8 +116,8 @@ func Test_GetPluginAssets(t *testing.T) {
})
t.Run("Given a request for an non-existing plugin", func(t *testing.T) {
service := &pluginStore{
plugins: map[string]*plugins.Plugin{},
service := &fakePluginStore{
plugins: map[string]plugins.PluginDTO{},
}
l := &logger{}
@ -137,8 +137,8 @@ func Test_GetPluginAssets(t *testing.T) {
})
t.Run("Given a request for a core plugin's file", func(t *testing.T) {
service := &pluginStore{
plugins: map[string]*plugins.Plugin{
service := &fakePluginStore{
plugins: map[string]plugins.PluginDTO{
pluginID: {
Class: plugins.Core,
},
@ -185,16 +185,6 @@ func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern strin
})
}
type pluginStore struct {
plugins.Store
plugins map[string]*plugins.Plugin
}
func (pm *pluginStore) Plugin(id string) *plugins.Plugin {
return pm.plugins[id]
}
type logger struct {
log.Logger

@ -330,8 +330,8 @@ func (uss *UsageStats) updateTotalStats(ctx context.Context) {
}
func (uss *UsageStats) ShouldBeReported(dsType string) bool {
ds := uss.pluginStore.Plugin(dsType)
if ds == nil {
ds, exists := uss.pluginStore.Plugin(context.TODO(), dsType)
if !exists {
return false
}
@ -367,13 +367,13 @@ func (uss *UsageStats) GetUsageStatsId(ctx context.Context) string {
}
func (uss *UsageStats) appCount() int {
return len(uss.pluginStore.Plugins(plugins.App))
return len(uss.pluginStore.Plugins(context.TODO(), plugins.App))
}
func (uss *UsageStats) panelCount() int {
return len(uss.pluginStore.Plugins(plugins.Panel))
return len(uss.pluginStore.Plugins(context.TODO(), plugins.Panel))
}
func (uss *UsageStats) dataSourceCount() int {
return len(uss.pluginStore.Plugins(plugins.DataSource))
return len(uss.pluginStore.Plugins(context.TODO(), plugins.DataSource))
}

@ -554,15 +554,17 @@ func TestMetrics(t *testing.T) {
type fakePluginStore struct {
plugins.Store
plugins map[string]*plugins.Plugin
plugins map[string]plugins.PluginDTO
}
func (pr fakePluginStore) Plugin(pluginID string) *plugins.Plugin {
return pr.plugins[pluginID]
func (pr fakePluginStore) Plugin(_ context.Context, pluginID string) (plugins.PluginDTO, bool) {
p, exists := pr.plugins[pluginID]
return p, exists
}
func (pr fakePluginStore) Plugins(pluginTypes ...plugins.Type) []*plugins.Plugin {
var result []*plugins.Plugin
func (pr fakePluginStore) Plugins(_ context.Context, pluginTypes ...plugins.Type) []plugins.PluginDTO {
var result []plugins.PluginDTO
for _, v := range pr.plugins {
for _, t := range pluginTypes {
if v.Type == t {
@ -578,7 +580,7 @@ func setupSomeDataSourcePlugins(t *testing.T, uss *UsageStats) {
t.Helper()
uss.pluginStore = &fakePluginStore{
plugins: map[string]*plugins.Plugin{
plugins: map[string]plugins.PluginDTO{
models.DS_ES: {
Signature: "internal",
},

@ -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

@ -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}
}

@ -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()

@ -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)
}

@ -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)
}
}

@ -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)
}

@ -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
}

@ -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
}

@ -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 {

@ -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 (

@ -69,8 +69,8 @@ func (p *DataSourceProxyService) ProxyDatasourceRequestWithID(c *models.ReqConte
}
// find plugin
plugin := p.pluginStore.Plugin(ds.Type)
if plugin == nil {
plugin, exists := p.pluginStore.Plugin(c.Req.Context(), ds.Type)
if !exists {
c.JsonApiErr(http.StatusNotFound, "Unable to find datasource plugin", err)
return
}

@ -414,8 +414,8 @@ type GrafanaLive struct {
}
func (g *GrafanaLive) getStreamPlugin(pluginID string) (backend.StreamHandler, error) {
plugin := g.pluginStore.Plugin(pluginID)
if plugin == nil {
plugin, exists := g.pluginStore.Plugin(context.TODO(), pluginID)
if !exists {
return nil, fmt.Errorf("plugin not found: %s", pluginID)
}
if plugin.SupportsStreaming() {

@ -1,6 +1,7 @@
package plugins
import (
"context"
"fmt"
"io/ioutil"
"os"
@ -113,7 +114,7 @@ func (cr *configReaderImpl) validatePluginsConfig(apps []*pluginsAsConfig) error
}
for _, app := range apps[i].Apps {
if p := cr.pluginStore.Plugin(app.PluginID); p == nil {
if _, exists := cr.pluginStore.Plugin(context.TODO(), app.PluginID); !exists {
return fmt.Errorf("plugin not installed: %q", app.PluginID)
}
}

@ -1,6 +1,7 @@
package plugins
import (
"context"
"os"
"testing"
@ -47,7 +48,7 @@ func TestConfigReader(t *testing.T) {
t.Run("Can read correct properties", func(t *testing.T) {
pm := fakePluginStore{
apps: map[string]*plugins.Plugin{
apps: map[string]plugins.PluginDTO{
"test-plugin": {},
"test-plugin-2": {},
},
@ -90,9 +91,11 @@ func TestConfigReader(t *testing.T) {
type fakePluginStore struct {
plugins.Store
apps map[string]*plugins.Plugin
apps map[string]plugins.PluginDTO
}
func (pr fakePluginStore) Plugin(pluginID string) *plugins.Plugin {
return pr.apps[pluginID]
func (pr fakePluginStore) Plugin(_ context.Context, pluginID string) (plugins.PluginDTO, bool) {
p, exists := pr.apps[pluginID]
return p, exists
}

Loading…
Cancel
Save