diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go index 284b72c0b95..134e10d411f 100644 --- a/pkg/api/app_routes.go +++ b/pkg/api/app_routes.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) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index bb23728f66b..9333e97fbb4 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -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}) } diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 4510795e8e9..2228e15105f 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -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) } diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index b7b3b2a6528..dcef9df59aa 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -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"` diff --git a/pkg/api/fakes.go b/pkg/api/fakes.go index a9f58ad4b80..23b5a89c52d 100644 --- a/pkg/api/fakes.go +++ b/pkg/api/fakes.go @@ -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 { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 297986c9a30..d61d3f05c43 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -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 diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index abc4bd46250..aa889a4bd8f 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -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, + 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 def.IsApp() { - dto.Enabled = def.AutoEnabled - dto.Pinned = def.AutoEnabled + 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, ¬Found) { @@ -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 { diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 1f80a5d4620..8b6689a9fbc 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -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 diff --git a/pkg/infra/usagestats/service/usage_stats.go b/pkg/infra/usagestats/service/usage_stats.go index 4f9fbedfdca..f72bdf8d80d 100644 --- a/pkg/infra/usagestats/service/usage_stats.go +++ b/pkg/infra/usagestats/service/usage_stats.go @@ -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)) } diff --git a/pkg/infra/usagestats/service/usage_stats_test.go b/pkg/infra/usagestats/service/usage_stats_test.go index cb912710aa4..41b6718f0c5 100644 --- a/pkg/infra/usagestats/service/usage_stats_test.go +++ b/pkg/infra/usagestats/service/usage_stats_test.go @@ -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", }, diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index b43c02aebc5..5824cc8273b 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -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 diff --git a/pkg/plugins/manager/dashboards.go b/pkg/plugins/manager/dashboards.go index cdef5e7e2f9..8ba40430c6b 100644 --- a/pkg/plugins/manager/dashboards.go +++ b/pkg/plugins/manager/dashboards.go @@ -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} } diff --git a/pkg/plugins/manager/manager.go b/pkg/plugins/manager/manager.go index 520cd2cf9af..9bb490f6c66 100644 --- a/pkg/plugins/manager/manager.go +++ b/pkg/plugins/manager/manager.go @@ -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() diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index 0b1d05806cd..d7283b7649d 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -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) } diff --git a/pkg/plugins/manager/manager_test.go b/pkg/plugins/manager/manager_test.go index 9b23d9eb7a9..f996bc45a9f 100644 --- a/pkg/plugins/manager/manager_test.go +++ b/pkg/plugins/manager/manager_test.go @@ -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) } } diff --git a/pkg/plugins/manager/store.go b/pkg/plugins/manager/store.go new file mode 100644 index 00000000000..db60e9f21f4 --- /dev/null +++ b/pkg/plugins/manager/store.go @@ -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) +} diff --git a/pkg/plugins/manager/update_checker.go b/pkg/plugins/manager/update_checker.go index f6c8ddafccb..4eee58a7a4a 100644 --- a/pkg/plugins/manager/update_checker.go +++ b/pkg/plugins/manager/update_checker.go @@ -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 } diff --git a/pkg/plugins/plugincontext/plugincontext.go b/pkg/plugins/plugincontext/plugincontext.go index bcea5ea034c..0c3d943a965 100644 --- a/pkg/plugins/plugincontext/plugincontext.go +++ b/pkg/plugins/plugincontext/plugincontext.go @@ -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 } diff --git a/pkg/plugins/plugindashboards/service.go b/pkg/plugins/plugindashboards/service.go index 18f41f49f59..8aa4cd2b98c 100644 --- a/pkg/plugins/plugindashboards/service.go +++ b/pkg/plugins/plugindashboards/service.go @@ -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 { diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 57f2a913750..0c5a9d7930f 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -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 ( diff --git a/pkg/services/datasourceproxy/datasourceproxy.go b/pkg/services/datasourceproxy/datasourceproxy.go index cc51da693b1..752aee7aa89 100644 --- a/pkg/services/datasourceproxy/datasourceproxy.go +++ b/pkg/services/datasourceproxy/datasourceproxy.go @@ -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 } diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index 70d530104cf..b94940adeea 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -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() { diff --git a/pkg/services/provisioning/plugins/config_reader.go b/pkg/services/provisioning/plugins/config_reader.go index b707fe79614..84c6e679498 100644 --- a/pkg/services/provisioning/plugins/config_reader.go +++ b/pkg/services/provisioning/plugins/config_reader.go @@ -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) } } diff --git a/pkg/services/provisioning/plugins/config_reader_test.go b/pkg/services/provisioning/plugins/config_reader_test.go index a5335d697d0..48fe8bd8091 100644 --- a/pkg/services/provisioning/plugins/config_reader_test.go +++ b/pkg/services/provisioning/plugins/config_reader_test.go @@ -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 }