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

* toying around

* fix refs

* remove unused fields

* go further

* add context

* ensure streaming handler is set
This commit is contained in:
Will Browne
2021-11-17 12:04:22 +01:00
committed by GitHub
parent dbb8246b6b
commit 2e3e7a7e55
24 changed files with 494 additions and 353 deletions
+2 -1
View File
@@ -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)
+5 -5
View File
@@ -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})
}
+5 -5
View File
@@ -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)
}
+4 -4
View File
@@ -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"`
+22 -5
View File
@@ -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 {
+13 -13
View File
@@ -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
+39 -38
View File
@@ -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, &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 {
+13 -23
View File
@@ -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