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:
+20
-7
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user