Backend Plugins: Provide proper plugin config to plugins (#21985)

Properly provides plugin configs to backend plugins.
Uses v0.16.0 of grafana-plugin-sdk-go-

Ref #21512
Ref #19667
This commit is contained in:
Marcus Efraimsson
2020-02-19 19:17:05 +01:00
committed by GitHub
parent f82a6aa0d0
commit 9d7c74ef91
20 changed files with 1223 additions and 179 deletions
+20 -7
View File
@@ -275,15 +275,28 @@ func (hs *HTTPServer) CallDatasourceResource(c *m.ReqContext) Response {
if err != nil {
return Error(500, "Failed to read request body", err)
}
jsonDataBytes, err := ds.JsonData.MarshalJSON()
if err != nil {
return Error(500, "Failed to marshal JSON data to bytes", err)
}
req := backendplugin.CallResourceRequest{
Config: backendplugin.PluginConfig{
OrgID: c.OrgId,
PluginID: plugin.Id,
Instance: &backendplugin.PluginInstance{
ID: ds.Id,
Name: ds.Name,
Type: ds.Type,
URL: ds.Url,
OrgID: c.OrgId,
PluginID: plugin.Id,
PluginType: plugin.Type,
JSONData: jsonDataBytes,
DecryptedSecureJSONData: ds.DecryptedValues(),
Updated: ds.Updated,
DataSourceConfig: &backendplugin.DataSourceConfig{
ID: ds.Id,
Name: ds.Name,
URL: ds.Url,
Database: ds.Database,
User: ds.User,
BasicAuthEnabled: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
},
},
Path: c.Params("*"),
+59 -14
View File
@@ -1,18 +1,20 @@
package api
import (
"encoding/json"
"sort"
"time"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
)
func (hs *HTTPServer) GetPluginList(c *m.ReqContext) Response {
func (hs *HTTPServer) GetPluginList(c *models.ReqContext) Response {
typeFilter := c.Query("type")
enabledFilter := c.Query("enabled")
embeddedFilter := c.Query("embedded")
@@ -85,7 +87,7 @@ func (hs *HTTPServer) GetPluginList(c *m.ReqContext) Response {
return JSON(200, result)
}
func GetPluginSettingByID(c *m.ReqContext) Response {
func GetPluginSettingByID(c *models.ReqContext) Response {
pluginID := c.Params(":pluginId")
def, exists := plugins.Plugins[pluginID]
@@ -108,9 +110,9 @@ func GetPluginSettingByID(c *m.ReqContext) Response {
State: def.State,
}
query := m.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}
query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
if err != m.ErrPluginSettingNotFound {
if err != models.ErrPluginSettingNotFound {
return Error(500, "Failed to get login settings", nil)
}
} else {
@@ -122,7 +124,7 @@ func GetPluginSettingByID(c *m.ReqContext) Response {
return JSON(200, dto)
}
func UpdatePluginSetting(c *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response {
func UpdatePluginSetting(c *models.ReqContext, cmd models.UpdatePluginSettingCmd) Response {
pluginID := c.Params(":pluginId")
cmd.OrgId = c.OrgId
@@ -139,7 +141,7 @@ func UpdatePluginSetting(c *m.ReqContext, cmd m.UpdatePluginSettingCmd) Response
return Success("Plugin settings updated")
}
func GetPluginDashboards(c *m.ReqContext) Response {
func GetPluginDashboards(c *models.ReqContext) Response {
pluginID := c.Params(":pluginId")
list, err := plugins.GetPluginDashboards(c.OrgId, pluginID)
@@ -154,7 +156,7 @@ func GetPluginDashboards(c *m.ReqContext) Response {
return JSON(200, list)
}
func GetPluginMarkdown(c *m.ReqContext) Response {
func GetPluginMarkdown(c *models.ReqContext) Response {
pluginID := c.Params(":pluginId")
name := c.Params(":name")
@@ -180,7 +182,7 @@ func GetPluginMarkdown(c *m.ReqContext) Response {
return resp
}
func ImportDashboard(c *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Response {
func ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDashboardCommand) Response {
if apiCmd.PluginId == "" && apiCmd.Dashboard == nil {
return Error(422, "Dashboard must be set", nil)
}
@@ -204,7 +206,7 @@ func ImportDashboard(c *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Respon
}
// /api/plugins/:pluginId/health
func (hs *HTTPServer) CheckHealth(c *m.ReqContext) Response {
func (hs *HTTPServer) CheckHealth(c *models.ReqContext) Response {
pluginID := c.Params("pluginId")
resp, err := hs.BackendPluginManager.CheckHealth(c.Req.Context(), pluginID)
if err != nil {
@@ -236,21 +238,45 @@ func (hs *HTTPServer) CheckHealth(c *m.ReqContext) Response {
}
// /api/plugins/:pluginId/resources/*
func (hs *HTTPServer) CallResource(c *m.ReqContext) Response {
func (hs *HTTPServer) CallResource(c *models.ReqContext) Response {
pluginID := c.Params("pluginId")
_, exists := plugins.Plugins[pluginID]
plugin, exists := plugins.Plugins[pluginID]
if !exists {
return Error(404, "Plugin not found, no installed plugin with that id", nil)
}
var jsonDataBytes []byte
var decryptedSecureJSONData map[string]string
var updated time.Time
ps, err := hs.getCachedPluginSettings(pluginID, c.SignedInUser)
if err != nil {
if err != models.ErrPluginSettingNotFound {
return Error(500, "Failed to get plugin settings", err)
}
} else {
jsonDataBytes, err = json.Marshal(&ps.JsonData)
if err != nil {
return Error(500, "Failed to marshal JSON data to bytes", err)
}
decryptedSecureJSONData = ps.DecryptedValues()
updated = ps.Updated
}
body, err := c.Req.Body().Bytes()
if err != nil {
return Error(500, "Failed to read request body", err)
}
req := backendplugin.CallResourceRequest{
Config: backendplugin.PluginConfig{
OrgID: c.OrgId,
PluginID: pluginID,
OrgID: c.OrgId,
PluginID: plugin.Id,
PluginType: plugin.Type,
JSONData: jsonDataBytes,
DecryptedSecureJSONData: decryptedSecureJSONData,
Updated: updated,
},
Path: c.Params("*"),
Method: c.Req.Method,
@@ -273,3 +299,22 @@ func (hs *HTTPServer) CallResource(c *m.ReqContext) Response {
header: resp.Headers,
}
}
func (hs *HTTPServer) getCachedPluginSettings(pluginID string, user *models.SignedInUser) (*models.PluginSetting, error) {
cacheKey := "plugin-setting-" + pluginID
if cached, found := hs.CacheService.Get(cacheKey); found {
ps := cached.(*models.PluginSetting)
if ps.OrgId == user.OrgId {
return ps, nil
}
}
query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: user.OrgId}
if err := hs.Bus.Dispatch(&query); err != nil {
return nil, err
}
hs.CacheService.Set(cacheKey, query.Result, time.Second*5)
return query.Result, nil
}
+37
View File
@@ -0,0 +1,37 @@
package models
var pluginSettingDecryptionCache = secureJSONDecryptionCache{
cache: make(map[int64]cachedDecryptedJSON),
}
// DecryptedValues returns cached decrypted values from secureJsonData.
func (ps *PluginSetting) DecryptedValues() map[string]string {
pluginSettingDecryptionCache.Lock()
defer pluginSettingDecryptionCache.Unlock()
if item, present := pluginSettingDecryptionCache.cache[ps.Id]; present && ps.Updated.Equal(item.updated) {
return item.json
}
json := ps.SecureJsonData.Decrypt()
pluginSettingDecryptionCache.cache[ps.Id] = cachedDecryptedJSON{
updated: ps.Updated,
json: json,
}
return json
}
// DecryptedValue returns cached decrypted value from cached secureJsonData.
func (ps *PluginSetting) DecryptedValue(key string) (string, bool) {
value, exists := ps.DecryptedValues()[key]
return value, exists
}
// ClearPluginSettingDecryptionCache clears the datasource decryption cache.
func ClearPluginSettingDecryptionCache() {
pluginSettingDecryptionCache.Lock()
defer pluginSettingDecryptionCache.Unlock()
pluginSettingDecryptionCache.cache = make(map[int64]cachedDecryptedJSON)
}
+62
View File
@@ -0,0 +1,62 @@
package models
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/securejsondata"
)
func TestPluginSettingDecryptionCache(t *testing.T) {
t.Run("When plugin settings hasn't been updated, encrypted JSON should be fetched from cache", func(t *testing.T) {
ClearPluginSettingDecryptionCache()
ps := PluginSetting{
Id: 1,
JsonData: map[string]interface{}{},
SecureJsonData: securejsondata.GetEncryptedJsonData(map[string]string{
"password": "password",
}),
}
// Populate cache
password, ok := ps.DecryptedValue("password")
require.Equal(t, "password", password)
require.True(t, ok)
ps.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{
"password": "",
})
require.Equal(t, "password", password)
require.True(t, ok)
})
t.Run("When plugin settings is updated, encrypted JSON should not be fetched from cache", func(t *testing.T) {
ClearPluginSettingDecryptionCache()
ps := PluginSetting{
Id: 1,
JsonData: map[string]interface{}{},
SecureJsonData: securejsondata.GetEncryptedJsonData(map[string]string{
"password": "password",
}),
}
// Populate cache
password, ok := ps.DecryptedValue("password")
require.Equal(t, "password", password)
require.True(t, ok)
ps.SecureJsonData = securejsondata.GetEncryptedJsonData(map[string]string{
"password": "",
})
ps.Updated = time.Now()
password, ok = ps.DecryptedValue("password")
require.Empty(t, password)
require.True(t, ok)
})
}
+22 -1
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"time"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
"github.com/prometheus/client_golang/prometheus"
@@ -205,13 +206,33 @@ func (p *BackendPlugin) callResource(ctx context.Context, req CallResourceReques
}
protoReq := &pluginv2.CallResource_Request{
Config: &pluginv2.PluginConfig{},
Config: &pluginv2.PluginConfig{
OrgId: req.Config.OrgID,
PluginId: req.Config.PluginID,
PluginType: req.Config.PluginType,
JsonData: req.Config.JSONData,
DecryptedSecureJsonData: req.Config.DecryptedSecureJSONData,
UpdatedMS: req.Config.Updated.UnixNano() / int64(time.Millisecond),
},
Path: req.Path,
Method: req.Method,
Url: req.URL,
Headers: reqHeaders,
Body: req.Body,
}
if req.Config.DataSourceConfig != nil {
protoReq.Config.DatasourceConfig = &pluginv2.DataSourceConfig{
Id: req.Config.DataSourceConfig.ID,
Name: req.Config.DataSourceConfig.Name,
Url: req.Config.DataSourceConfig.URL,
Database: req.Config.DataSourceConfig.Database,
User: req.Config.DataSourceConfig.User,
BasicAuthEnabled: req.Config.DataSourceConfig.BasicAuthEnabled,
BasicAuthUser: req.Config.DataSourceConfig.BasicAuthUser,
}
}
protoResp, err := p.core.CallResource(ctx, protoReq)
if err != nil {
if st, ok := status.FromError(err); ok {
+16 -9
View File
@@ -3,6 +3,7 @@ package backendplugin
import (
"encoding/json"
"strconv"
"time"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)
@@ -54,18 +55,24 @@ func checkHealthResultFromProto(protoResp *pluginv2.CheckHealth_Response) *Check
}
}
type PluginInstance struct {
ID int64
Name string
Type string
URL string
JSONData json.RawMessage
type DataSourceConfig struct {
ID int64
Name string
URL string
User string
Database string
BasicAuthEnabled bool
BasicAuthUser string
}
type PluginConfig struct {
PluginID string
OrgID int64
Instance *PluginInstance
OrgID int64
PluginID string
PluginType string
JSONData json.RawMessage
DecryptedSecureJSONData map[string]string
Updated time.Time
DataSourceConfig *DataSourceConfig
}
type CallResourceRequest struct {
@@ -2,6 +2,7 @@ package wrapper
import (
"context"
"time"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
@@ -12,30 +13,40 @@ import (
"github.com/grafana/grafana/pkg/tsdb"
)
func NewDatasourcePluginWrapperV2(log log.Logger, plugin backendplugin.DatasourcePlugin) *DatasourcePluginWrapperV2 {
return &DatasourcePluginWrapperV2{DatasourcePlugin: plugin, logger: log}
func NewDatasourcePluginWrapperV2(log log.Logger, pluginId, pluginType string, plugin backendplugin.DatasourcePlugin) *DatasourcePluginWrapperV2 {
return &DatasourcePluginWrapperV2{DatasourcePlugin: plugin, logger: log, pluginId: pluginId, pluginType: pluginType}
}
type DatasourcePluginWrapperV2 struct {
backendplugin.DatasourcePlugin
logger log.Logger
logger log.Logger
pluginId string
pluginType string
}
func (tw *DatasourcePluginWrapperV2) Query(ctx context.Context, ds *models.DataSource, query *tsdb.TsdbQuery) (*tsdb.Response, error) {
jsonData, err := ds.JsonData.MarshalJSON()
jsonDataBytes, err := ds.JsonData.MarshalJSON()
if err != nil {
return nil, err
}
pbQuery := &pluginv2.DataQueryRequest{
Config: &pluginv2.PluginConfig{
Name: ds.Name,
Type: ds.Type,
Url: ds.Url,
Id: ds.Id,
OrgId: ds.OrgId,
JsonData: string(jsonData),
DecryptedSecureJsonData: ds.SecureJsonData.Decrypt(),
PluginId: tw.pluginId,
PluginType: tw.pluginType,
UpdatedMS: ds.Updated.UnixNano() / int64(time.Millisecond),
JsonData: jsonDataBytes,
DecryptedSecureJsonData: ds.DecryptedValues(),
DatasourceConfig: &pluginv2.DataSourceConfig{
Id: ds.Id,
Name: ds.Name,
Url: ds.Url,
Database: ds.Database,
User: ds.User,
BasicAuthEnabled: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
},
},
Queries: []*pluginv2.DataQuery{},
}
+1 -1
View File
@@ -70,7 +70,7 @@ func (p *DataSourcePlugin) onLegacyPluginStart(pluginID string, client *backendp
func (p *DataSourcePlugin) onPluginStart(pluginID string, client *backendplugin.Client, logger log.Logger) error {
if client.DatasourcePlugin != nil {
tsdb.RegisterTsdbQueryEndpoint(pluginID, func(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
return wrapper.NewDatasourcePluginWrapperV2(logger, client.DatasourcePlugin), nil
return wrapper.NewDatasourcePluginWrapperV2(logger, p.Id, p.Type, client.DatasourcePlugin), nil
})
}
+8 -2
View File
@@ -55,7 +55,7 @@ func (p *TransformPlugin) onPluginStart(pluginID string, client *backendplugin.C
if client.DatasourcePlugin != nil {
tsdb.RegisterTsdbQueryEndpoint(pluginID, func(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
return wrapper.NewDatasourcePluginWrapperV2(logger, client.DatasourcePlugin), nil
return wrapper.NewDatasourcePluginWrapperV2(logger, p.Id, p.Type, client.DatasourcePlugin), nil
})
}
@@ -122,9 +122,15 @@ func (s *transformCallback) DataQuery(ctx context.Context, req *pluginv2.DataQue
return nil, fmt.Errorf("zero queries found in datasource request")
}
datasourceID := int64(0)
if req.Config.DatasourceConfig != nil {
datasourceID = req.Config.DatasourceConfig.Id
}
getDsInfo := &models.GetDataSourceByIdQuery{
Id: req.Config.Id,
OrgId: req.Config.OrgId,
Id: datasourceID,
}
if err := bus.Dispatch(getDsInfo); err != nil {