Introduce TSDB service (#31520)
* Introduce TSDB service Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> Co-authored-by: Erik Sundell <erik.sundell87@gmail.com> Co-authored-by: Will Browne <will.browne@grafana.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.org> Co-authored-by: Will Browne <wbrowne@users.noreply.github.com> Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
This commit is contained in:
203 files changed
+5269
-4776
No files matched your search
@@ -0,0 +1,42 @@
|
||||
// Package adapters contains plugin SDK adapters.
|
||||
package adapters
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
// ModelToInstanceSettings converts a models.DataSource to a backend.DataSourceInstanceSettings.
|
||||
func ModelToInstanceSettings(ds *models.DataSource) (*backend.DataSourceInstanceSettings, error) {
|
||||
jsonDataBytes, err := ds.JsonData.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &backend.DataSourceInstanceSettings{
|
||||
ID: ds.Id,
|
||||
Name: ds.Name,
|
||||
URL: ds.Url,
|
||||
Database: ds.Database,
|
||||
User: ds.User,
|
||||
BasicAuthEnabled: ds.BasicAuth,
|
||||
BasicAuthUser: ds.BasicAuthUser,
|
||||
JSONData: jsonDataBytes,
|
||||
DecryptedSecureJSONData: ds.DecryptedValues(),
|
||||
Updated: ds.Updated,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BackendUserFromSignedInUser converts Grafana's SignedInUser model
|
||||
// to the backend plugin's model.
|
||||
func BackendUserFromSignedInUser(su *models.SignedInUser) *backend.User {
|
||||
if su == nil {
|
||||
return nil
|
||||
}
|
||||
return &backend.User{
|
||||
Login: su.Login,
|
||||
Name: su.Name,
|
||||
Email: su.Email,
|
||||
Role: string(su.OrgRole),
|
||||
}
|
||||
}
|
||||
+12
-14
@@ -59,33 +59,29 @@ type JwtTokenAuth struct {
|
||||
Params map[string]string `json:"params"`
|
||||
}
|
||||
|
||||
func (app *AppPlugin) Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) error {
|
||||
func (app *AppPlugin) Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) (
|
||||
interface{}, error) {
|
||||
if err := decoder.Decode(app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := app.registerPlugin(base); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if app.Backend {
|
||||
cmd := ComposePluginStartCommand(app.Executable)
|
||||
fullpath := filepath.Join(app.PluginDir, cmd)
|
||||
fullpath := filepath.Join(base.PluginDir, cmd)
|
||||
factory := grpcplugin.NewBackendPlugin(app.Id, fullpath, grpcplugin.PluginStartFuncs{})
|
||||
if err := backendPluginManager.Register(app.Id, factory); err != nil {
|
||||
return errutil.Wrapf(err, "failed to register backend plugin")
|
||||
return nil, errutil.Wrapf(err, "failed to register backend plugin")
|
||||
}
|
||||
}
|
||||
|
||||
Apps[app.Id] = app
|
||||
return nil
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (app *AppPlugin) initApp() {
|
||||
app.initFrontendPlugin()
|
||||
func (app *AppPlugin) InitApp(panels map[string]*PanelPlugin, dataSources map[string]*DataSourcePlugin) []*PluginStaticRoute {
|
||||
staticRoutes := app.InitFrontendPlugin()
|
||||
|
||||
// check if we have child panels
|
||||
for _, panel := range Panels {
|
||||
for _, panel := range panels {
|
||||
if strings.HasPrefix(panel.PluginDir, app.PluginDir) {
|
||||
panel.setPathsBasedOnApp(app)
|
||||
app.FoundChildPlugins = append(app.FoundChildPlugins, &PluginInclude{
|
||||
@@ -97,7 +93,7 @@ func (app *AppPlugin) initApp() {
|
||||
}
|
||||
|
||||
// check if we have child datasources
|
||||
for _, ds := range DataSources {
|
||||
for _, ds := range dataSources {
|
||||
if strings.HasPrefix(ds.PluginDir, app.PluginDir) {
|
||||
ds.setPathsBasedOnApp(app)
|
||||
app.FoundChildPlugins = append(app.FoundChildPlugins, &PluginInclude{
|
||||
@@ -120,4 +116,6 @@ func (app *AppPlugin) initApp() {
|
||||
app.DefaultNavUrl = setting.AppSubUrl + "/dashboard/db/" + include.Slug
|
||||
}
|
||||
}
|
||||
|
||||
return staticRoutes
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Package backendplugin contains backend plugin related logic.
|
||||
package backendplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
)
|
||||
|
||||
// PluginFactoryFunc is a function type for creating a Plugin.
|
||||
type PluginFactoryFunc func(pluginID string, logger log.Logger, env []string) (Plugin, error)
|
||||
@@ -6,14 +6,16 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/instrumentation"
|
||||
)
|
||||
|
||||
// corePlugin represents a plugin that's part of Grafana core.
|
||||
type corePlugin struct {
|
||||
pluginID string
|
||||
logger log.Logger
|
||||
isDataPlugin bool
|
||||
pluginID string
|
||||
logger log.Logger
|
||||
backend.CheckHealthHandler
|
||||
backend.CallResourceHandler
|
||||
backend.QueryDataHandler
|
||||
@@ -21,7 +23,7 @@ type corePlugin struct {
|
||||
|
||||
// New returns a new backendplugin.PluginFactoryFunc for creating a core (built-in) backendplugin.Plugin.
|
||||
func New(opts backend.ServeOpts) backendplugin.PluginFactoryFunc {
|
||||
return backendplugin.PluginFactoryFunc(func(pluginID string, logger log.Logger, env []string) (backendplugin.Plugin, error) {
|
||||
return func(pluginID string, logger log.Logger, env []string) (backendplugin.Plugin, error) {
|
||||
return &corePlugin{
|
||||
pluginID: pluginID,
|
||||
logger: logger,
|
||||
@@ -29,7 +31,7 @@ func New(opts backend.ServeOpts) backendplugin.PluginFactoryFunc {
|
||||
CallResourceHandler: opts.CallResourceHandler,
|
||||
QueryDataHandler: opts.QueryDataHandler,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (cp *corePlugin) PluginID() string {
|
||||
@@ -40,11 +42,21 @@ func (cp *corePlugin) Logger() log.Logger {
|
||||
return cp.logger
|
||||
}
|
||||
|
||||
func (cp *corePlugin) CanHandleDataQueries() bool {
|
||||
return cp.isDataPlugin
|
||||
}
|
||||
|
||||
func (cp *corePlugin) DataQuery(ctx context.Context, dsInfo *models.DataSource,
|
||||
tsdbQuery plugins.DataQuery) (plugins.DataResponse, error) {
|
||||
// TODO: Inline the adapter, since it shouldn't be necessary
|
||||
adapter := newQueryEndpointAdapter(cp.pluginID, cp.logger, instrumentation.InstrumentQueryDataHandler(
|
||||
cp.QueryDataHandler))
|
||||
return adapter.DataQuery(ctx, dsInfo, tsdbQuery)
|
||||
}
|
||||
|
||||
func (cp *corePlugin) Start(ctx context.Context) error {
|
||||
if cp.QueryDataHandler != nil {
|
||||
tsdb.RegisterTsdbQueryEndpoint(cp.pluginID, func(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
||||
return newQueryEndpointAdapter(cp.pluginID, cp.logger, backendplugin.InstrumentQueryDataHandler(cp.QueryDataHandler)), nil
|
||||
})
|
||||
cp.isDataPlugin = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -36,11 +36,13 @@ func TestCorePlugin(t *testing.T) {
|
||||
checkHealthCalled := false
|
||||
callResourceCalled := false
|
||||
factory := coreplugin.New(backend.ServeOpts{
|
||||
CheckHealthHandler: backend.CheckHealthHandlerFunc(func(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
CheckHealthHandler: backend.CheckHealthHandlerFunc(func(ctx context.Context,
|
||||
req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
checkHealthCalled = true
|
||||
return nil, nil
|
||||
}),
|
||||
CallResourceHandler: backend.CallResourceHandlerFunc(func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
CallResourceHandler: backend.CallResourceHandlerFunc(func(ctx context.Context,
|
||||
req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
callResourceCalled = true
|
||||
return nil
|
||||
}),
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins/datasource/wrapper"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/adapters"
|
||||
)
|
||||
|
||||
func newQueryEndpointAdapter(pluginID string, logger log.Logger, handler backend.QueryDataHandler) tsdb.TsdbQueryEndpoint {
|
||||
func newQueryEndpointAdapter(pluginID string, logger log.Logger, handler backend.QueryDataHandler) plugins.DataPlugin {
|
||||
return &queryEndpointAdapter{
|
||||
pluginID: pluginID,
|
||||
logger: logger,
|
||||
@@ -45,17 +45,18 @@ func modelToInstanceSettings(ds *models.DataSource) (*backend.DataSourceInstance
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *queryEndpointAdapter) Query(ctx context.Context, ds *models.DataSource, query *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
func (a *queryEndpointAdapter) DataQuery(ctx context.Context, ds *models.DataSource, query plugins.DataQuery) (
|
||||
plugins.DataResponse, error) {
|
||||
instanceSettings, err := modelToInstanceSettings(ds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return plugins.DataResponse{}, err
|
||||
}
|
||||
|
||||
req := &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{
|
||||
OrgID: ds.OrgId,
|
||||
PluginID: a.pluginID,
|
||||
User: wrapper.BackendUserFromSignedInUser(query.User),
|
||||
User: adapters.BackendUserFromSignedInUser(query.User),
|
||||
DataSourceInstanceSettings: instanceSettings,
|
||||
},
|
||||
Queries: []backend.DataQuery{},
|
||||
@@ -65,11 +66,11 @@ func (a *queryEndpointAdapter) Query(ctx context.Context, ds *models.DataSource,
|
||||
for _, q := range query.Queries {
|
||||
modelJSON, err := q.Model.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return plugins.DataResponse{}, err
|
||||
}
|
||||
req.Queries = append(req.Queries, backend.DataQuery{
|
||||
RefID: q.RefId,
|
||||
Interval: time.Duration(q.IntervalMs) * time.Millisecond,
|
||||
RefID: q.RefID,
|
||||
Interval: time.Duration(q.IntervalMS) * time.Millisecond,
|
||||
MaxDataPoints: q.MaxDataPoints,
|
||||
TimeRange: backend.TimeRange{
|
||||
From: query.TimeRange.GetFromAsTimeUTC(),
|
||||
@@ -82,16 +83,16 @@ func (a *queryEndpointAdapter) Query(ctx context.Context, ds *models.DataSource,
|
||||
|
||||
resp, err := a.handler.QueryData(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return plugins.DataResponse{}, err
|
||||
}
|
||||
|
||||
tR := &tsdb.Response{
|
||||
Results: make(map[string]*tsdb.QueryResult, len(resp.Responses)),
|
||||
tR := plugins.DataResponse{
|
||||
Results: make(map[string]plugins.DataQueryResult, len(resp.Responses)),
|
||||
}
|
||||
|
||||
for refID, r := range resp.Responses {
|
||||
qr := &tsdb.QueryResult{
|
||||
RefId: refID,
|
||||
qr := plugins.DataQueryResult{
|
||||
RefID: refID,
|
||||
}
|
||||
|
||||
for _, f := range r.Frames {
|
||||
@@ -100,7 +101,7 @@ func (a *queryEndpointAdapter) Query(ctx context.Context, ds *models.DataSource,
|
||||
}
|
||||
}
|
||||
|
||||
qr.Dataframes = tsdb.NewDecodedDataFrames(r.Frames)
|
||||
qr.Dataframes = plugins.NewDecodedDataFrames(r.Frames)
|
||||
|
||||
if r.Error != nil {
|
||||
qr.Error = r.Error
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package backendplugin
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrPluginNotRegistered error returned when plugin not registered.
|
||||
ErrPluginNotRegistered = errors.New("plugin not registered")
|
||||
// ErrHealthCheckFailed error returned when health check failed.
|
||||
ErrHealthCheckFailed = errors.New("health check failed")
|
||||
// ErrPluginUnavailable error returned when plugin is unavailable.
|
||||
ErrPluginUnavailable = errors.New("plugin unavailable")
|
||||
// ErrMethodNotImplemented error returned when plugin method not implemented.
|
||||
ErrMethodNotImplemented = errors.New("method not implemented")
|
||||
)
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/instrumentation"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
)
|
||||
|
||||
@@ -75,7 +76,7 @@ func instrumentDatasourcePluginV1(plugin datasourceV1.DatasourcePlugin) datasour
|
||||
|
||||
return datasourceV1QueryFunc(func(ctx context.Context, req *datasourceV1.DatasourceRequest) (*datasourceV1.DatasourceResponse, error) {
|
||||
var resp *datasourceV1.DatasourceResponse
|
||||
err := backendplugin.InstrumentQueryDataRequest(req.Datasource.Type, func() (innerErr error) {
|
||||
err := instrumentation.InstrumentQueryDataRequest(req.Datasource.Type, func() (innerErr error) {
|
||||
resp, innerErr = plugin.Query(ctx, req)
|
||||
return
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/instrumentation"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
@@ -170,7 +171,7 @@ func instrumentDataClient(plugin grpcplugin.DataClient) grpcplugin.DataClient {
|
||||
|
||||
return dataClientQueryDataFunc(func(ctx context.Context, req *pluginv2.QueryDataRequest, opts ...grpc.CallOption) (*pluginv2.QueryDataResponse, error) {
|
||||
var resp *pluginv2.QueryDataResponse
|
||||
err := backendplugin.InstrumentQueryDataRequest(req.PluginContext.PluginId, func() (innerErr error) {
|
||||
err := instrumentation.InstrumentQueryDataRequest(req.PluginContext.PluginId, func() (innerErr error) {
|
||||
resp, innerErr = plugin.QueryData(ctx, req)
|
||||
return
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@ type grpcPlugin struct {
|
||||
|
||||
// newPlugin allocates and returns a new gRPC (external) backendplugin.Plugin.
|
||||
func newPlugin(descriptor PluginDescriptor) backendplugin.PluginFactoryFunc {
|
||||
return backendplugin.PluginFactoryFunc(func(pluginID string, logger log.Logger, env []string) (backendplugin.Plugin, error) {
|
||||
return func(pluginID string, logger log.Logger, env []string) (backendplugin.Plugin, error) {
|
||||
return &grpcPlugin{
|
||||
descriptor: descriptor,
|
||||
logger: logger,
|
||||
@@ -36,7 +36,11 @@ func newPlugin(descriptor PluginDescriptor) backendplugin.PluginFactoryFunc {
|
||||
return plugin.NewClient(newClientConfig(descriptor.executablePath, env, logger, descriptor.versionedPlugins))
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (p *grpcPlugin) CanHandleDataQueries() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *grpcPlugin) PluginID() string {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package backendplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
// Manager manages backend plugins.
|
||||
type Manager interface {
|
||||
// Register registers a backend plugin
|
||||
Register(pluginID string, factory PluginFactoryFunc) error
|
||||
// StartPlugin starts a non-managed backend plugin
|
||||
StartPlugin(ctx context.Context, pluginID string) error
|
||||
// CollectMetrics collects metrics from a registered backend plugin.
|
||||
CollectMetrics(ctx context.Context, pluginID string) (*backend.CollectMetricsResult, error)
|
||||
// CheckHealth checks the health of a registered backend plugin.
|
||||
CheckHealth(ctx context.Context, pCtx backend.PluginContext) (*backend.CheckHealthResult, error)
|
||||
// CallResource calls a plugin resource.
|
||||
CallResource(pluginConfig backend.PluginContext, ctx *models.ReqContext, path string)
|
||||
// GetDataPlugin gets a DataPlugin with a certain ID or nil if it doesn't exist.
|
||||
// TODO: interface{} is the return type in order to break a dependency cycle. Should be plugins.DataPlugin.
|
||||
GetDataPlugin(pluginID string) interface{}
|
||||
}
|
||||
|
||||
// Plugin is the backend plugin interface.
|
||||
type Plugin interface {
|
||||
PluginID() string
|
||||
Logger() log.Logger
|
||||
Start(ctx context.Context) error
|
||||
Stop(ctx context.Context) error
|
||||
IsManaged() bool
|
||||
Exited() bool
|
||||
CanHandleDataQueries() bool
|
||||
backend.CollectMetricsHandler
|
||||
backend.CheckHealthHandler
|
||||
backend.CallResourceHandler
|
||||
}
|
||||
+9
-5
@@ -1,4 +1,5 @@
|
||||
package backendplugin
|
||||
// Package instrumentation contains backend plugin instrumentation logic.
|
||||
package instrumentation
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -48,19 +49,22 @@ func instrumentPluginRequest(pluginID string, endpoint string, fn func() error)
|
||||
return err
|
||||
}
|
||||
|
||||
func instrumentCollectMetrics(pluginID string, fn func() error) error {
|
||||
// InstrumentCollectMetrics instruments collectMetrics.
|
||||
func InstrumentCollectMetrics(pluginID string, fn func() error) error {
|
||||
return instrumentPluginRequest(pluginID, "collectMetrics", fn)
|
||||
}
|
||||
|
||||
func instrumentCheckHealthRequest(pluginID string, fn func() error) error {
|
||||
// InstrumentCheckHealthRequest instruments checkHealth.
|
||||
func InstrumentCheckHealthRequest(pluginID string, fn func() error) error {
|
||||
return instrumentPluginRequest(pluginID, "checkHealth", fn)
|
||||
}
|
||||
|
||||
func instrumentCallResourceRequest(pluginID string, fn func() error) error {
|
||||
// InstrumentCallResourceRequest instruments callResource.
|
||||
func InstrumentCallResourceRequest(pluginID string, fn func() error) error {
|
||||
return instrumentPluginRequest(pluginID, "callResource", fn)
|
||||
}
|
||||
|
||||
// InstrumentQueryDataRequest instruments success rate and latency of query data request.
|
||||
// InstrumentQueryDataRequest instruments success rate and latency of query data requests.
|
||||
func InstrumentQueryDataRequest(pluginID string, fn func() error) error {
|
||||
return instrumentPluginRequest(pluginID, "queryData", fn)
|
||||
}
|
||||
+43
-46
@@ -1,4 +1,4 @@
|
||||
package backendplugin
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -15,53 +15,31 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/instrumentation"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
"github.com/grafana/grafana/pkg/util/proxyutil"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrPluginNotRegistered error returned when plugin not registered.
|
||||
ErrPluginNotRegistered = errors.New("plugin not registered")
|
||||
// ErrHealthCheckFailed error returned when health check failed.
|
||||
ErrHealthCheckFailed = errors.New("health check failed")
|
||||
// ErrPluginUnavailable error returned when plugin is unavailable.
|
||||
ErrPluginUnavailable = errors.New("plugin unavailable")
|
||||
// ErrMethodNotImplemented error returned when plugin method not implemented.
|
||||
ErrMethodNotImplemented = errors.New("method not implemented")
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.RegisterServiceWithPriority(&manager{}, registry.MediumHigh)
|
||||
}
|
||||
|
||||
// Manager manages backend plugins.
|
||||
type Manager interface {
|
||||
// Register registers a backend plugin
|
||||
Register(pluginID string, factory PluginFactoryFunc) error
|
||||
// StartPlugin starts a non-managed backend plugin
|
||||
StartPlugin(ctx context.Context, pluginID string) error
|
||||
// CollectMetrics collects metrics from a registered backend plugin.
|
||||
CollectMetrics(ctx context.Context, pluginID string) (*backend.CollectMetricsResult, error)
|
||||
// CheckHealth checks the health of a registered backend plugin.
|
||||
CheckHealth(ctx context.Context, pCtx backend.PluginContext) (*backend.CheckHealthResult, error)
|
||||
// CallResource calls a plugin resource.
|
||||
CallResource(pluginConfig backend.PluginContext, ctx *models.ReqContext, path string)
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
License models.Licensing `inject:""`
|
||||
PluginRequestValidator models.PluginRequestValidator `inject:""`
|
||||
pluginsMu sync.RWMutex
|
||||
plugins map[string]Plugin
|
||||
plugins map[string]backendplugin.Plugin
|
||||
logger log.Logger
|
||||
pluginSettings map[string]pluginSettings
|
||||
}
|
||||
|
||||
func (m *manager) Init() error {
|
||||
m.plugins = make(map[string]Plugin)
|
||||
m.plugins = make(map[string]backendplugin.Plugin)
|
||||
m.logger = log.New("plugins.backend")
|
||||
m.pluginSettings = extractPluginSettings(m.Cfg)
|
||||
|
||||
@@ -76,7 +54,7 @@ func (m *manager) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Register registers a backend plugin
|
||||
func (m *manager) Register(pluginID string, factory PluginFactoryFunc) error {
|
||||
func (m *manager) Register(pluginID string, factory backendplugin.PluginFactoryFunc) error {
|
||||
m.logger.Debug("Registering backend plugin", "pluginId", pluginID)
|
||||
m.pluginsMu.Lock()
|
||||
defer m.pluginsMu.Unlock()
|
||||
@@ -121,6 +99,19 @@ func (m *manager) Register(pluginID string, factory PluginFactoryFunc) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) GetDataPlugin(pluginID string) interface{} {
|
||||
plugin := m.plugins[pluginID]
|
||||
if plugin == nil || !plugin.CanHandleDataQueries() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if dataPlugin, ok := plugin.(plugins.DataPlugin); ok {
|
||||
return dataPlugin
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// start starts all managed backend plugins
|
||||
func (m *manager) start(ctx context.Context) {
|
||||
m.pluginsMu.RLock()
|
||||
@@ -143,7 +134,7 @@ func (m *manager) StartPlugin(ctx context.Context, pluginID string) error {
|
||||
p, registered := m.plugins[pluginID]
|
||||
m.pluginsMu.RUnlock()
|
||||
if !registered {
|
||||
return ErrPluginNotRegistered
|
||||
return backendplugin.ErrPluginNotRegistered
|
||||
}
|
||||
|
||||
if p.IsManaged() {
|
||||
@@ -160,7 +151,7 @@ func (m *manager) stop(ctx context.Context) {
|
||||
var wg sync.WaitGroup
|
||||
for _, p := range m.plugins {
|
||||
wg.Add(1)
|
||||
go func(p Plugin, ctx context.Context) {
|
||||
go func(p backendplugin.Plugin, ctx context.Context) {
|
||||
defer wg.Done()
|
||||
p.Logger().Debug("Stopping plugin")
|
||||
if err := p.Stop(ctx); err != nil {
|
||||
@@ -179,11 +170,11 @@ func (m *manager) CollectMetrics(ctx context.Context, pluginID string) (*backend
|
||||
m.pluginsMu.RUnlock()
|
||||
|
||||
if !registered {
|
||||
return nil, ErrPluginNotRegistered
|
||||
return nil, backendplugin.ErrPluginNotRegistered
|
||||
}
|
||||
|
||||
var resp *backend.CollectMetricsResult
|
||||
err := instrumentCollectMetrics(p.PluginID(), func() (innerErr error) {
|
||||
err := instrumentation.InstrumentCollectMetrics(p.PluginID(), func() (innerErr error) {
|
||||
resp, innerErr = p.CollectMetrics(ctx)
|
||||
return
|
||||
})
|
||||
@@ -214,25 +205,25 @@ func (m *manager) CheckHealth(ctx context.Context, pluginContext backend.PluginC
|
||||
m.pluginsMu.RUnlock()
|
||||
|
||||
if !registered {
|
||||
return nil, ErrPluginNotRegistered
|
||||
return nil, backendplugin.ErrPluginNotRegistered
|
||||
}
|
||||
|
||||
var resp *backend.CheckHealthResult
|
||||
err = instrumentCheckHealthRequest(p.PluginID(), func() (innerErr error) {
|
||||
err = instrumentation.InstrumentCheckHealthRequest(p.PluginID(), func() (innerErr error) {
|
||||
resp, innerErr = p.CheckHealth(ctx, &backend.CheckHealthRequest{PluginContext: pluginContext})
|
||||
return
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrMethodNotImplemented) {
|
||||
if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrPluginUnavailable) {
|
||||
if errors.Is(err, backendplugin.ErrPluginUnavailable) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, errutil.Wrap("failed to check plugin health", ErrHealthCheckFailed)
|
||||
return nil, errutil.Wrap("failed to check plugin health", backendplugin.ErrHealthCheckFailed)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
@@ -248,7 +239,7 @@ func (m *manager) callResourceInternal(w http.ResponseWriter, req *http.Request,
|
||||
m.pluginsMu.RUnlock()
|
||||
|
||||
if !registered {
|
||||
return ErrPluginNotRegistered
|
||||
return backendplugin.ErrPluginNotRegistered
|
||||
}
|
||||
|
||||
keepCookieModel := keepCookiesJSONModel{}
|
||||
@@ -276,7 +267,7 @@ func (m *manager) callResourceInternal(w http.ResponseWriter, req *http.Request,
|
||||
Body: body,
|
||||
}
|
||||
|
||||
return instrumentCallResourceRequest(p.PluginID(), func() error {
|
||||
return instrumentation.InstrumentCallResourceRequest(p.PluginID(), func() error {
|
||||
childCtx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
stream := newCallResourceResponseStream(childCtx)
|
||||
@@ -336,12 +327,12 @@ func (m *manager) CallResource(pCtx backend.PluginContext, reqCtx *models.ReqCon
|
||||
}
|
||||
|
||||
func handleCallResourceError(err error, reqCtx *models.ReqContext) {
|
||||
if errors.Is(err, ErrPluginUnavailable) {
|
||||
if errors.Is(err, backendplugin.ErrPluginUnavailable) {
|
||||
reqCtx.JsonApiErr(503, "Plugin unavailable", err)
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrMethodNotImplemented) {
|
||||
if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
|
||||
reqCtx.JsonApiErr(404, "Not found", err)
|
||||
return
|
||||
}
|
||||
@@ -349,7 +340,7 @@ func handleCallResourceError(err error, reqCtx *models.ReqContext) {
|
||||
reqCtx.JsonApiErr(500, "Failed to call resource", err)
|
||||
}
|
||||
|
||||
func flushStream(plugin Plugin, stream CallResourceClientResponseStream, w http.ResponseWriter) error {
|
||||
func flushStream(plugin backendplugin.Plugin, stream callResourceClientResponseStream, w http.ResponseWriter) error {
|
||||
processedStreams := 0
|
||||
|
||||
for {
|
||||
@@ -404,12 +395,12 @@ func flushStream(plugin Plugin, stream CallResourceClientResponseStream, w http.
|
||||
}
|
||||
}
|
||||
|
||||
func startPluginAndRestartKilledProcesses(ctx context.Context, p Plugin) error {
|
||||
func startPluginAndRestartKilledProcesses(ctx context.Context, p backendplugin.Plugin) error {
|
||||
if err := p.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func(ctx context.Context, p Plugin) {
|
||||
go func(ctx context.Context, p backendplugin.Plugin) {
|
||||
if err := restartKilledProcess(ctx, p); err != nil {
|
||||
p.Logger().Error("Attempt to restart killed plugin process failed", "error", err)
|
||||
}
|
||||
@@ -418,7 +409,7 @@ func startPluginAndRestartKilledProcesses(ctx context.Context, p Plugin) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func restartKilledProcess(ctx context.Context, p Plugin) error {
|
||||
func restartKilledProcess(ctx context.Context, p backendplugin.Plugin) error {
|
||||
ticker := time.NewTicker(time.Second * 1)
|
||||
|
||||
for {
|
||||
@@ -442,3 +433,9 @@ func restartKilledProcess(ctx context.Context, p Plugin) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// callResourceClientResponseStream is used for receiving resource call responses.
|
||||
type callResourceClientResponseStream interface {
|
||||
Recv() (*backend.CallResourceResponse, error)
|
||||
Close() error
|
||||
}
|
||||
+26
-20
@@ -1,4 +1,4 @@
|
||||
package backendplugin
|
||||
package manager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -22,19 +23,19 @@ func TestManager(t *testing.T) {
|
||||
newManagerScenario(t, false, func(t *testing.T, ctx *managerScenarioCtx) {
|
||||
t.Run("Unregistered plugin scenario", func(t *testing.T) {
|
||||
err := ctx.manager.StartPlugin(context.Background(), testPluginID)
|
||||
require.Equal(t, ErrPluginNotRegistered, err)
|
||||
require.Equal(t, backendplugin.ErrPluginNotRegistered, err)
|
||||
|
||||
_, err = ctx.manager.CollectMetrics(context.Background(), testPluginID)
|
||||
require.Equal(t, ErrPluginNotRegistered, err)
|
||||
require.Equal(t, backendplugin.ErrPluginNotRegistered, err)
|
||||
|
||||
_, err = ctx.manager.CheckHealth(context.Background(), backend.PluginContext{PluginID: testPluginID})
|
||||
require.Equal(t, ErrPluginNotRegistered, err)
|
||||
require.Equal(t, backendplugin.ErrPluginNotRegistered, err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/test", nil)
|
||||
require.NoError(t, err)
|
||||
w := httptest.NewRecorder()
|
||||
err = ctx.manager.callResourceInternal(w, req, backend.PluginContext{PluginID: testPluginID})
|
||||
require.Equal(t, ErrPluginNotRegistered, err)
|
||||
require.Equal(t, backendplugin.ErrPluginNotRegistered, err)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -121,12 +122,12 @@ func TestManager(t *testing.T) {
|
||||
t.Run("Unimplemented handlers", func(t *testing.T) {
|
||||
t.Run("Collect metrics should return method not implemented error", func(t *testing.T) {
|
||||
_, err = ctx.manager.CollectMetrics(context.Background(), testPluginID)
|
||||
require.Equal(t, ErrMethodNotImplemented, err)
|
||||
require.Equal(t, backendplugin.ErrMethodNotImplemented, err)
|
||||
})
|
||||
|
||||
t.Run("Check health should return method not implemented error", func(t *testing.T) {
|
||||
_, err = ctx.manager.CheckHealth(context.Background(), backend.PluginContext{PluginID: testPluginID})
|
||||
require.Equal(t, ErrMethodNotImplemented, err)
|
||||
require.Equal(t, backendplugin.ErrMethodNotImplemented, err)
|
||||
})
|
||||
|
||||
t.Run("Call resource should return method not implemented error", func(t *testing.T) {
|
||||
@@ -134,17 +135,17 @@ func TestManager(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
w := httptest.NewRecorder()
|
||||
err = ctx.manager.callResourceInternal(w, req, backend.PluginContext{PluginID: testPluginID})
|
||||
require.Equal(t, ErrMethodNotImplemented, err)
|
||||
require.Equal(t, backendplugin.ErrMethodNotImplemented, err)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Implemented handlers", func(t *testing.T) {
|
||||
t.Run("Collect metrics should return expected result", func(t *testing.T) {
|
||||
ctx.plugin.CollectMetricsHandlerFunc = backend.CollectMetricsHandlerFunc(func(ctx context.Context) (*backend.CollectMetricsResult, error) {
|
||||
ctx.plugin.CollectMetricsHandlerFunc = func(ctx context.Context) (*backend.CollectMetricsResult, error) {
|
||||
return &backend.CollectMetricsResult{
|
||||
PrometheusMetrics: []byte("hello"),
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
res, err := ctx.manager.CollectMetrics(context.Background(), testPluginID)
|
||||
require.NoError(t, err)
|
||||
@@ -156,13 +157,13 @@ func TestManager(t *testing.T) {
|
||||
json := []byte(`{
|
||||
"key": "value"
|
||||
}`)
|
||||
ctx.plugin.CheckHealthHandlerFunc = backend.CheckHealthHandlerFunc(func(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
ctx.plugin.CheckHealthHandlerFunc = func(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
return &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusOk,
|
||||
Message: "All good",
|
||||
JSONDetails: json,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
res, err := ctx.manager.CheckHealth(context.Background(), backend.PluginContext{PluginID: testPluginID})
|
||||
require.NoError(t, err)
|
||||
@@ -173,11 +174,12 @@ func TestManager(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Call resource should return expected response", func(t *testing.T) {
|
||||
ctx.plugin.CallResourceHandlerFunc = backend.CallResourceHandlerFunc(func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
ctx.plugin.CallResourceHandlerFunc = func(ctx context.Context,
|
||||
req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
return sender.Send(&backend.CallResourceResponse{
|
||||
Status: http.StatusOK,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/test", bytes.NewReader([]byte{}))
|
||||
require.NoError(t, err)
|
||||
@@ -270,7 +272,7 @@ type managerScenarioCtx struct {
|
||||
cfg *setting.Cfg
|
||||
license *testLicensingService
|
||||
manager *manager
|
||||
factory PluginFactoryFunc
|
||||
factory backendplugin.PluginFactoryFunc
|
||||
plugin *testPlugin
|
||||
env []string
|
||||
}
|
||||
@@ -293,7 +295,7 @@ func newManagerScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *m
|
||||
err := ctx.manager.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx.factory = PluginFactoryFunc(func(pluginID string, logger log.Logger, env []string) (Plugin, error) {
|
||||
ctx.factory = func(pluginID string, logger log.Logger, env []string) (backendplugin.Plugin, error) {
|
||||
ctx.plugin = &testPlugin{
|
||||
pluginID: pluginID,
|
||||
logger: logger,
|
||||
@@ -302,7 +304,7 @@ func newManagerScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *m
|
||||
ctx.env = env
|
||||
|
||||
return ctx.plugin, nil
|
||||
})
|
||||
}
|
||||
|
||||
fn(t, ctx)
|
||||
}
|
||||
@@ -328,6 +330,10 @@ func (tp *testPlugin) Logger() log.Logger {
|
||||
return tp.logger
|
||||
}
|
||||
|
||||
func (tp *testPlugin) CanHandleDataQueries() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (tp *testPlugin) Start(ctx context.Context) error {
|
||||
tp.mutex.Lock()
|
||||
defer tp.mutex.Unlock()
|
||||
@@ -364,7 +370,7 @@ func (tp *testPlugin) CollectMetrics(ctx context.Context) (*backend.CollectMetri
|
||||
return tp.CollectMetricsHandlerFunc(ctx)
|
||||
}
|
||||
|
||||
return nil, ErrMethodNotImplemented
|
||||
return nil, backendplugin.ErrMethodNotImplemented
|
||||
}
|
||||
|
||||
func (tp *testPlugin) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
@@ -372,7 +378,7 @@ func (tp *testPlugin) CheckHealth(ctx context.Context, req *backend.CheckHealthR
|
||||
return tp.CheckHealthHandlerFunc(ctx, req)
|
||||
}
|
||||
|
||||
return nil, ErrMethodNotImplemented
|
||||
return nil, backendplugin.ErrMethodNotImplemented
|
||||
}
|
||||
|
||||
func (tp *testPlugin) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
@@ -380,7 +386,7 @@ func (tp *testPlugin) CallResource(ctx context.Context, req *backend.CallResourc
|
||||
return tp.CallResourceHandlerFunc(ctx, req, sender)
|
||||
}
|
||||
|
||||
return ErrMethodNotImplemented
|
||||
return backendplugin.ErrMethodNotImplemented
|
||||
}
|
||||
|
||||
type testLicensingService struct {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package backendplugin
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package backendplugin
|
||||
package manager
|
||||
|
||||
import (
|
||||
"sort"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package backendplugin
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,30 +0,0 @@
|
||||
package backendplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
)
|
||||
|
||||
// Plugin backend plugin interface.
|
||||
type Plugin interface {
|
||||
PluginID() string
|
||||
Logger() log.Logger
|
||||
Start(ctx context.Context) error
|
||||
Stop(ctx context.Context) error
|
||||
IsManaged() bool
|
||||
Exited() bool
|
||||
backend.CollectMetricsHandler
|
||||
backend.CheckHealthHandler
|
||||
backend.CallResourceHandler
|
||||
}
|
||||
|
||||
// PluginFactoryFunc factory for creating a Plugin.
|
||||
type PluginFactoryFunc func(pluginID string, logger log.Logger, env []string) (Plugin, error)
|
||||
|
||||
// CallResourceClientResponseStream is used for receiving resource call responses.
|
||||
type CallResourceClientResponseStream interface {
|
||||
Recv() (*backend.CallResourceResponse, error)
|
||||
Close() error
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDashboardImport(t *testing.T) {
|
||||
pluginScenario("When importing a plugin dashboard", t, func() {
|
||||
origNewDashboardService := dashboards.NewService
|
||||
mock := &dashboards.FakeDashboardService{}
|
||||
dashboards.MockDashboardService(mock)
|
||||
|
||||
cmd := ImportDashboardCommand{
|
||||
PluginId: "test-app",
|
||||
Path: "dashboards/connections.json",
|
||||
OrgId: 1,
|
||||
User: &models.SignedInUser{UserId: 1, OrgRole: models.ROLE_ADMIN},
|
||||
Inputs: []ImportDashboardInput{
|
||||
{Name: "*", Type: "datasource", Value: "graphite"},
|
||||
},
|
||||
}
|
||||
|
||||
err := ImportDashboard(&cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("should install dashboard", func() {
|
||||
So(cmd.Result, ShouldNotBeNil)
|
||||
|
||||
resultStr, _ := mock.SavedDashboards[0].Dashboard.Data.EncodePretty()
|
||||
expectedBytes, _ := ioutil.ReadFile("testdata/test-app/dashboards/connections_result.json")
|
||||
expectedJson, _ := simplejson.NewJson(expectedBytes)
|
||||
expectedStr, _ := expectedJson.EncodePretty()
|
||||
|
||||
So(string(resultStr), ShouldEqual, string(expectedStr))
|
||||
|
||||
panel := mock.SavedDashboards[0].Dashboard.Data.Get("rows").GetIndex(0).Get("panels").GetIndex(0)
|
||||
So(panel.Get("datasource").MustString(), ShouldEqual, "graphite")
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
dashboards.NewService = origNewDashboardService
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When evaling dashboard template", t, func() {
|
||||
template, _ := simplejson.NewJson([]byte(`{
|
||||
"__inputs": [
|
||||
{
|
||||
"name": "DS_NAME",
|
||||
"type": "datasource"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"prop": "${DS_NAME}_${DS_NAME}"
|
||||
}
|
||||
}`))
|
||||
|
||||
evaluator := &DashTemplateEvaluator{
|
||||
template: template,
|
||||
inputs: []ImportDashboardInput{
|
||||
{Name: "*", Type: "datasource", Value: "my-server"},
|
||||
},
|
||||
}
|
||||
|
||||
res, err := evaluator.Eval()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("should render template", func() {
|
||||
So(res.GetPath("test", "prop").MustString(), ShouldEqual, "my-server_my-server")
|
||||
})
|
||||
|
||||
Convey("should not include inputs in output", func() {
|
||||
inputs := res.Get("__inputs")
|
||||
So(inputs.Interface(), ShouldBeNil)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func pluginScenario(desc string, t *testing.T, fn func()) {
|
||||
Convey("Given a plugin", t, func() {
|
||||
pm := &PluginManager{
|
||||
Cfg: &setting.Cfg{
|
||||
FeatureToggles: map[string]bool{},
|
||||
PluginSettings: setting.PluginSettings{
|
||||
"test-app": map[string]string{
|
||||
"path": "testdata/test-app",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := pm.Init()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey(desc, fn)
|
||||
})
|
||||
}
|
||||
@@ -1,14 +1,5 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type PluginDashboardInfoDTO struct {
|
||||
PluginId string `json:"pluginId"`
|
||||
Title string `json:"title"`
|
||||
@@ -24,97 +15,3 @@ type PluginDashboardInfoDTO struct {
|
||||
Path string `json:"path"`
|
||||
Removed bool `json:"removed"`
|
||||
}
|
||||
|
||||
func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) {
|
||||
plugin, exists := Plugins[pluginId]
|
||||
|
||||
if !exists {
|
||||
return nil, PluginNotFoundError{pluginId}
|
||||
}
|
||||
|
||||
result := make([]*PluginDashboardInfoDTO, 0)
|
||||
|
||||
// load current dashboards
|
||||
query := models.GetDashboardsByPluginIdQuery{OrgId: orgId, PluginId: pluginId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingMatches := make(map[int64]bool)
|
||||
|
||||
for _, include := range plugin.Includes {
|
||||
if include.Type != PluginTypeDashboard {
|
||||
continue
|
||||
}
|
||||
|
||||
res := &PluginDashboardInfoDTO{}
|
||||
var dashboard *models.Dashboard
|
||||
var err error
|
||||
|
||||
if dashboard, err = loadPluginDashboard(plugin.Id, include.Path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.Path = include.Path
|
||||
res.PluginId = plugin.Id
|
||||
res.Title = dashboard.Title
|
||||
res.Revision = dashboard.Data.Get("revision").MustInt64(1)
|
||||
|
||||
// find existing dashboard
|
||||
for _, existingDash := range query.Result {
|
||||
if existingDash.Slug == dashboard.Slug {
|
||||
res.DashboardId = existingDash.Id
|
||||
res.Imported = true
|
||||
res.ImportedUri = "db/" + existingDash.Slug
|
||||
res.ImportedUrl = existingDash.GetUrl()
|
||||
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
|
||||
existingMatches[existingDash.Id] = true
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, res)
|
||||
}
|
||||
|
||||
// find deleted dashboards
|
||||
for _, dash := range query.Result {
|
||||
if _, exists := existingMatches[dash.Id]; !exists {
|
||||
result = append(result, &PluginDashboardInfoDTO{
|
||||
Slug: dash.Slug,
|
||||
DashboardId: dash.Id,
|
||||
Removed: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func loadPluginDashboard(pluginId, path string) (*models.Dashboard, error) {
|
||||
plugin, exists := Plugins[pluginId]
|
||||
if !exists {
|
||||
return nil, PluginNotFoundError{pluginId}
|
||||
}
|
||||
|
||||
dashboardFilePath := filepath.Join(plugin.PluginDir, path)
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based
|
||||
// on plugin folder structure on disk and not user input. `path` comes from the
|
||||
// `plugin.json` configuration file for the loaded plugin
|
||||
reader, err := os.Open(dashboardFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := reader.Close(); err != nil {
|
||||
plog.Warn("Failed to close file", "path", dashboardFilePath, "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := simplejson.NewFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return models.NewDashboardFromJson(data), nil
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddEventListener(handlePluginStateChanged)
|
||||
}
|
||||
|
||||
func (pm *PluginManager) updateAppDashboards() {
|
||||
pm.log.Debug("Looking for App Dashboard Updates")
|
||||
|
||||
query := models.GetPluginSettingsQuery{OrgId: 0}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
pm.log.Error("Failed to get all plugin settings", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, pluginSetting := range query.Result {
|
||||
// ignore disabled plugins
|
||||
if !pluginSetting.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if pluginDef, exist := Plugins[pluginSetting.PluginId]; exist {
|
||||
if pluginDef.Info.Version != pluginSetting.PluginVersion {
|
||||
syncPluginDashboards(pluginDef, pluginSetting.OrgId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func autoUpdateAppDashboard(pluginDashInfo *PluginDashboardInfoDTO, orgId int64) error {
|
||||
dash, err := loadPluginDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plog.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev", pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision)
|
||||
updateCmd := ImportDashboardCommand{
|
||||
OrgId: orgId,
|
||||
PluginId: pluginDashInfo.PluginId,
|
||||
Overwrite: true,
|
||||
Dashboard: dash.Data,
|
||||
User: &models.SignedInUser{UserId: 0, OrgRole: models.ROLE_ADMIN},
|
||||
Path: pluginDashInfo.Path,
|
||||
}
|
||||
|
||||
return bus.Dispatch(&updateCmd)
|
||||
}
|
||||
|
||||
func syncPluginDashboards(pluginDef *PluginBase, orgId int64) {
|
||||
plog.Info("Syncing plugin dashboards to DB", "pluginId", pluginDef.Id)
|
||||
|
||||
// Get plugin dashboards
|
||||
dashboards, err := GetPluginDashboards(orgId, pluginDef.Id)
|
||||
|
||||
if err != nil {
|
||||
plog.Error("Failed to load app dashboards", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update dashboards with updated revisions
|
||||
for _, dash := range dashboards {
|
||||
// remove removed ones
|
||||
if dash.Removed {
|
||||
plog.Info("Deleting plugin dashboard", "pluginId", pluginDef.Id, "dashboard", dash.Slug)
|
||||
|
||||
deleteCmd := models.DeleteDashboardCommand{OrgId: orgId, Id: dash.DashboardId}
|
||||
if err := bus.Dispatch(&deleteCmd); err != nil {
|
||||
plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// update updated ones
|
||||
if dash.ImportedRevision != dash.Revision {
|
||||
if err := autoUpdateAppDashboard(dash, orgId); err != nil {
|
||||
plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.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}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
plog.Error("Failed to read plugin setting by id", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
appSetting := query.Result
|
||||
cmd := models.UpdatePluginSettingVersionCmd{
|
||||
OrgId: appSetting.OrgId,
|
||||
PluginId: appSetting.PluginId,
|
||||
PluginVersion: pluginDef.Info.Version,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
plog.Error("Failed to update plugin setting version", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handlePluginStateChanged(event *models.PluginStateChangedEvent) error {
|
||||
plog.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled)
|
||||
|
||||
if event.Enabled {
|
||||
syncPluginDashboards(Plugins[event.PluginId], event.OrgId)
|
||||
} else {
|
||||
query := models.GetDashboardsByPluginIdQuery{PluginId: event.PluginId, OrgId: event.OrgId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, dash := range query.Result {
|
||||
deleteCmd := models.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id}
|
||||
|
||||
plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
|
||||
|
||||
if err := bus.Dispatch(&deleteCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// DataFrames is an interface for retrieving encoded and decoded data frames.
|
||||
//
|
||||
// See NewDecodedDataFrames and NewEncodedDataFrames for more information.
|
||||
type DataFrames interface {
|
||||
// Encoded encodes Frames into a slice of []byte.
|
||||
// If an error occurs [][]byte will be nil.
|
||||
// The encoded result, if any, will be cached and returned next time Encoded is called.
|
||||
Encoded() ([][]byte, error)
|
||||
|
||||
// Decoded decodes a slice of Arrow encoded frames to data.Frames ([]*data.Frame).
|
||||
// If an error occurs Frames will be nil.
|
||||
// The decoded result, if any, will be cached and returned next time Decoded is called.
|
||||
Decoded() (data.Frames, error)
|
||||
}
|
||||
|
||||
type dataFrames struct {
|
||||
decoded data.Frames
|
||||
encoded [][]byte
|
||||
}
|
||||
|
||||
// NewDecodedDataFrames instantiates DataFrames from decoded frames.
|
||||
//
|
||||
// This should be the primary function for creating DataFrames if you're implementing a plugin.
|
||||
// In a Grafana alerting scenario it needs to operate on decoded frames, which is why this function is
|
||||
// preferrable. When encoded data frames are needed, e.g. returned from Grafana HTTP API, it will
|
||||
// happen automatically when MarshalJSON() is called.
|
||||
func NewDecodedDataFrames(decodedFrames data.Frames) DataFrames {
|
||||
return &dataFrames{
|
||||
decoded: decodedFrames,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEncodedDataFrames instantiates DataFrames from encoded frames.
|
||||
//
|
||||
// This one is primarily used for creating DataFrames when receiving encoded data frames from an external
|
||||
// plugin or similar. This may allow the encoded data frames to be returned to Grafana UI without any additional
|
||||
// decoding/encoding required. In Grafana alerting scenario it needs to operate on decoded data frames why encoded
|
||||
// frames needs to be decoded before usage.
|
||||
func NewEncodedDataFrames(encodedFrames [][]byte) DataFrames {
|
||||
return &dataFrames{
|
||||
encoded: encodedFrames,
|
||||
}
|
||||
}
|
||||
|
||||
func (df *dataFrames) Encoded() ([][]byte, error) {
|
||||
if df.encoded == nil {
|
||||
encoded, err := df.decoded.MarshalArrow()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
df.encoded = encoded
|
||||
}
|
||||
|
||||
return df.encoded, nil
|
||||
}
|
||||
|
||||
func (df *dataFrames) Decoded() (data.Frames, error) {
|
||||
if df.decoded == nil {
|
||||
decoded, err := data.UnmarshalArrowFrames(df.encoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
df.decoded = decoded
|
||||
}
|
||||
|
||||
return df.decoded, nil
|
||||
}
|
||||
|
||||
func (df *dataFrames) MarshalJSON() ([]byte, error) {
|
||||
encoded, err := df.Encoded()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use a configuration that's compatible with the standard library
|
||||
// to minimize the risk of introducing bugs. This will make sure
|
||||
// that map keys is ordered.
|
||||
jsonCfg := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
return jsonCfg.Marshal(encoded)
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/grpcplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/datasource/wrapper"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
@@ -32,47 +32,65 @@ type DataSourcePlugin struct {
|
||||
Backend bool `json:"backend,omitempty"`
|
||||
Executable string `json:"executable,omitempty"`
|
||||
SDK bool `json:"sdk,omitempty"`
|
||||
|
||||
client *grpcplugin.Client
|
||||
legacyClient *grpcplugin.LegacyClient
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (p *DataSourcePlugin) Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) error {
|
||||
func (p *DataSourcePlugin) Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) (
|
||||
interface{}, error) {
|
||||
if err := decoder.Decode(p); err != nil {
|
||||
return errutil.Wrapf(err, "Failed to decode datasource plugin")
|
||||
}
|
||||
|
||||
if err := p.registerPlugin(base); err != nil {
|
||||
return errutil.Wrapf(err, "Failed to register plugin")
|
||||
return nil, errutil.Wrapf(err, "Failed to decode datasource plugin")
|
||||
}
|
||||
|
||||
if p.Backend {
|
||||
cmd := ComposePluginStartCommand(p.Executable)
|
||||
fullpath := filepath.Join(p.PluginDir, cmd)
|
||||
fullpath := filepath.Join(base.PluginDir, cmd)
|
||||
factory := grpcplugin.NewBackendPlugin(p.Id, fullpath, grpcplugin.PluginStartFuncs{
|
||||
OnLegacyStart: p.onLegacyPluginStart,
|
||||
OnStart: p.onPluginStart,
|
||||
})
|
||||
if err := backendPluginManager.Register(p.Id, factory); err != nil {
|
||||
return errutil.Wrapf(err, "Failed to register backend plugin")
|
||||
return nil, errutil.Wrapf(err, "failed to register backend plugin")
|
||||
}
|
||||
}
|
||||
|
||||
DataSources[p.Id] = p
|
||||
return nil
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *DataSourcePlugin) DataQuery(ctx context.Context, dsInfo *models.DataSource, query DataQuery) (DataResponse, error) {
|
||||
if !p.CanHandleDataQueries() {
|
||||
return DataResponse{}, fmt.Errorf("plugin %q can't handle data queries", p.Id)
|
||||
}
|
||||
|
||||
if p.client != nil {
|
||||
endpoint := newDataSourcePluginWrapperV2(p.logger, p.Id, p.Type, p.client.DataPlugin)
|
||||
return endpoint.Query(ctx, dsInfo, query)
|
||||
}
|
||||
|
||||
endpoint := newDataSourcePluginWrapper(p.logger, p.legacyClient.DatasourcePlugin)
|
||||
return endpoint.Query(ctx, dsInfo, query)
|
||||
}
|
||||
|
||||
func (p *DataSourcePlugin) onLegacyPluginStart(pluginID string, client *grpcplugin.LegacyClient, logger log.Logger) error {
|
||||
tsdb.RegisterTsdbQueryEndpoint(pluginID, func(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
||||
return wrapper.NewDatasourcePluginWrapper(logger, client.DatasourcePlugin), nil
|
||||
})
|
||||
p.legacyClient = client
|
||||
p.logger = logger
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *DataSourcePlugin) CanHandleDataQueries() bool {
|
||||
return p.client != nil || p.legacyClient != nil
|
||||
}
|
||||
|
||||
func (p *DataSourcePlugin) onPluginStart(pluginID string, client *grpcplugin.Client, logger log.Logger) error {
|
||||
if client.DataPlugin != nil {
|
||||
tsdb.RegisterTsdbQueryEndpoint(pluginID, func(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
||||
return wrapper.NewDatasourcePluginWrapperV2(logger, p.Id, p.Type, client.DataPlugin), nil
|
||||
})
|
||||
if client.DataPlugin == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.client = client
|
||||
p.logger = logger
|
||||
|
||||
return nil
|
||||
}
|
||||
+30
-28
@@ -1,4 +1,4 @@
|
||||
package wrapper
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -10,10 +10,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
func NewDatasourcePluginWrapper(log log.Logger, plugin datasource.DatasourcePlugin) *DatasourcePluginWrapper {
|
||||
func newDataSourcePluginWrapper(log log.Logger, plugin datasource.DatasourcePlugin) *DatasourcePluginWrapper {
|
||||
return &DatasourcePluginWrapper{DatasourcePlugin: plugin, logger: log}
|
||||
}
|
||||
|
||||
@@ -22,10 +21,10 @@ type DatasourcePluginWrapper struct {
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (tw *DatasourcePluginWrapper) Query(ctx context.Context, ds *models.DataSource, query *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
func (tw *DatasourcePluginWrapper) Query(ctx context.Context, ds *models.DataSource, query DataQuery) (DataResponse, error) {
|
||||
jsonData, err := ds.JsonData.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return DataResponse{}, err
|
||||
}
|
||||
|
||||
pbQuery := &datasource.DatasourceRequest{
|
||||
@@ -48,31 +47,33 @@ func (tw *DatasourcePluginWrapper) Query(ctx context.Context, ds *models.DataSou
|
||||
}
|
||||
|
||||
for _, q := range query.Queries {
|
||||
modelJson, _ := q.Model.MarshalJSON()
|
||||
modelJson, err := q.Model.MarshalJSON()
|
||||
if err != nil {
|
||||
return DataResponse{}, err
|
||||
}
|
||||
|
||||
pbQuery.Queries = append(pbQuery.Queries, &datasource.Query{
|
||||
ModelJson: string(modelJson),
|
||||
IntervalMs: q.IntervalMs,
|
||||
RefId: q.RefId,
|
||||
IntervalMs: q.IntervalMS,
|
||||
RefId: q.RefID,
|
||||
MaxDataPoints: q.MaxDataPoints,
|
||||
})
|
||||
}
|
||||
|
||||
pbres, err := tw.DatasourcePlugin.Query(ctx, pbQuery)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return DataResponse{}, err
|
||||
}
|
||||
|
||||
res := &tsdb.Response{
|
||||
Results: map[string]*tsdb.QueryResult{},
|
||||
res := DataResponse{
|
||||
Results: map[string]DataQueryResult{},
|
||||
}
|
||||
|
||||
for _, r := range pbres.Results {
|
||||
qr := &tsdb.QueryResult{
|
||||
RefId: r.RefId,
|
||||
Series: []*tsdb.TimeSeries{},
|
||||
Tables: []*tsdb.Table{},
|
||||
qr := DataQueryResult{
|
||||
RefID: r.RefId,
|
||||
Series: []DataTimeSeries{},
|
||||
Tables: []DataTable{},
|
||||
}
|
||||
|
||||
if r.Error != "" {
|
||||
@@ -89,14 +90,14 @@ func (tw *DatasourcePluginWrapper) Query(ctx context.Context, ds *models.DataSou
|
||||
}
|
||||
|
||||
for _, s := range r.GetSeries() {
|
||||
points := tsdb.TimeSeriesPoints{}
|
||||
points := DataTimeSeriesPoints{}
|
||||
|
||||
for _, p := range s.Points {
|
||||
po := tsdb.NewTimePoint(null.FloatFrom(p.Value), float64(p.Timestamp))
|
||||
po := DataTimePoint{null.FloatFrom(p.Value), null.FloatFrom(float64(p.Timestamp))}
|
||||
points = append(points, po)
|
||||
}
|
||||
|
||||
qr.Series = append(qr.Series, &tsdb.TimeSeries{
|
||||
qr.Series = append(qr.Series, DataTimeSeries{
|
||||
Name: s.Name,
|
||||
Tags: s.Tags,
|
||||
Points: points,
|
||||
@@ -105,7 +106,7 @@ func (tw *DatasourcePluginWrapper) Query(ctx context.Context, ds *models.DataSou
|
||||
|
||||
mappedTables, err := tw.mapTables(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return DataResponse{}, err
|
||||
}
|
||||
qr.Tables = mappedTables
|
||||
|
||||
@@ -114,8 +115,9 @@ func (tw *DatasourcePluginWrapper) Query(ctx context.Context, ds *models.DataSou
|
||||
|
||||
return res, nil
|
||||
}
|
||||
func (tw *DatasourcePluginWrapper) mapTables(r *datasource.QueryResult) ([]*tsdb.Table, error) {
|
||||
var tables []*tsdb.Table
|
||||
|
||||
func (tw *DatasourcePluginWrapper) mapTables(r *datasource.QueryResult) ([]DataTable, error) {
|
||||
var tables []DataTable
|
||||
for _, t := range r.GetTables() {
|
||||
mappedTable, err := tw.mapTable(t)
|
||||
if err != nil {
|
||||
@@ -126,21 +128,21 @@ func (tw *DatasourcePluginWrapper) mapTables(r *datasource.QueryResult) ([]*tsdb
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func (tw *DatasourcePluginWrapper) mapTable(t *datasource.Table) (*tsdb.Table, error) {
|
||||
table := &tsdb.Table{}
|
||||
func (tw *DatasourcePluginWrapper) mapTable(t *datasource.Table) (DataTable, error) {
|
||||
table := DataTable{}
|
||||
for _, c := range t.GetColumns() {
|
||||
table.Columns = append(table.Columns, tsdb.TableColumn{
|
||||
table.Columns = append(table.Columns, DataTableColumn{
|
||||
Text: c.Name,
|
||||
})
|
||||
}
|
||||
|
||||
table.Rows = make([]tsdb.RowValues, 0)
|
||||
table.Rows = make([]DataRowValues, 0)
|
||||
for _, r := range t.GetRows() {
|
||||
row := tsdb.RowValues{}
|
||||
row := DataRowValues{}
|
||||
for _, rv := range r.Values {
|
||||
mappedRw, err := tw.mapRowValue(rv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return table, err
|
||||
}
|
||||
|
||||
row = append(row, mappedRw)
|
||||
+8
-8
@@ -1,16 +1,15 @@
|
||||
package wrapper
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-model/go/datasource"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMapTables(t *testing.T) {
|
||||
dpw := NewDatasourcePluginWrapper(log.New("test-logger"), nil)
|
||||
dpw := newDataSourcePluginWrapper(log.New("test-logger"), nil)
|
||||
var qr = &datasource.QueryResult{}
|
||||
qr.Tables = append(qr.Tables, &datasource.Table{
|
||||
Columns: []*datasource.TableColumn{},
|
||||
@@ -23,7 +22,7 @@ func TestMapTables(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMapTable(t *testing.T) {
|
||||
dpw := NewDatasourcePluginWrapper(log.New("test-logger"), nil)
|
||||
dpw := newDataSourcePluginWrapper(log.New("test-logger"), nil)
|
||||
|
||||
source := &datasource.Table{
|
||||
Columns: []*datasource.TableColumn{{Name: "column1"}, {Name: "column2"}},
|
||||
@@ -41,8 +40,8 @@ func TestMapTable(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
|
||||
want := &tsdb.Table{
|
||||
Columns: []tsdb.TableColumn{{Text: "column1"}, {Text: "column2"}},
|
||||
want := DataTable{
|
||||
Columns: []DataTableColumn{{Text: "column1"}, {Text: "column2"}},
|
||||
}
|
||||
have, err := dpw.mapTable(source)
|
||||
require.NoError(t, err)
|
||||
@@ -53,9 +52,10 @@ func TestMapTable(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMappingRowValue(t *testing.T) {
|
||||
dpw := NewDatasourcePluginWrapper(log.New("test-logger"), nil)
|
||||
dpw := newDataSourcePluginWrapper(log.New("test-logger"), nil)
|
||||
|
||||
boolRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BOOL, BoolValue: true})
|
||||
boolRowValue, err := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BOOL, BoolValue: true})
|
||||
require.NoError(t, err)
|
||||
haveBool, ok := boolRowValue.(bool)
|
||||
require.True(t, ok)
|
||||
require.True(t, haveBool)
|
||||
+16
-50
@@ -1,4 +1,4 @@
|
||||
package wrapper
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins/adapters"
|
||||
"github.com/grafana/grafana/pkg/services/oauthtoken"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
func NewDatasourcePluginWrapperV2(log log.Logger, pluginId, pluginType string, client grpcplugin.DataClient) *DatasourcePluginWrapperV2 {
|
||||
func newDataSourcePluginWrapperV2(log log.Logger, pluginId, pluginType string, client grpcplugin.DataClient) *DatasourcePluginWrapperV2 {
|
||||
return &DatasourcePluginWrapperV2{DataClient: client, logger: log, pluginId: pluginId, pluginType: pluginType}
|
||||
}
|
||||
|
||||
@@ -25,30 +25,10 @@ type DatasourcePluginWrapperV2 struct {
|
||||
pluginType string
|
||||
}
|
||||
|
||||
func ModelToInstanceSettings(ds *models.DataSource) (*backend.DataSourceInstanceSettings, error) {
|
||||
jsonDataBytes, err := ds.JsonData.MarshalJSON()
|
||||
func (tw *DatasourcePluginWrapperV2) Query(ctx context.Context, ds *models.DataSource, query DataQuery) (DataResponse, error) {
|
||||
instanceSettings, err := adapters.ModelToInstanceSettings(ds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &backend.DataSourceInstanceSettings{
|
||||
ID: ds.Id,
|
||||
Name: ds.Name,
|
||||
URL: ds.Url,
|
||||
Database: ds.Database,
|
||||
User: ds.User,
|
||||
BasicAuthEnabled: ds.BasicAuth,
|
||||
BasicAuthUser: ds.BasicAuthUser,
|
||||
JSONData: jsonDataBytes,
|
||||
DecryptedSecureJSONData: ds.DecryptedValues(),
|
||||
Updated: ds.Updated,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tw *DatasourcePluginWrapperV2) Query(ctx context.Context, ds *models.DataSource, query *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
instanceSettings, err := ModelToInstanceSettings(ds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return DataResponse{}, err
|
||||
}
|
||||
|
||||
if query.Headers == nil {
|
||||
@@ -66,7 +46,7 @@ func (tw *DatasourcePluginWrapperV2) Query(ctx context.Context, ds *models.DataS
|
||||
PluginContext: &pluginv2.PluginContext{
|
||||
OrgId: ds.OrgId,
|
||||
PluginId: tw.pluginId,
|
||||
User: backend.ToProto().User(BackendUserFromSignedInUser(query.User)),
|
||||
User: backend.ToProto().User(adapters.BackendUserFromSignedInUser(query.User)),
|
||||
DataSourceInstanceSettings: backend.ToProto().DataSourceInstanceSettings(instanceSettings),
|
||||
},
|
||||
Queries: []*pluginv2.DataQuery{},
|
||||
@@ -76,12 +56,12 @@ func (tw *DatasourcePluginWrapperV2) Query(ctx context.Context, ds *models.DataS
|
||||
for _, q := range query.Queries {
|
||||
modelJSON, err := q.Model.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return DataResponse{}, err
|
||||
}
|
||||
pbQuery.Queries = append(pbQuery.Queries, &pluginv2.DataQuery{
|
||||
Json: modelJSON,
|
||||
IntervalMS: q.IntervalMs,
|
||||
RefId: q.RefId,
|
||||
IntervalMS: q.IntervalMS,
|
||||
RefId: q.RefID,
|
||||
MaxDataPoints: q.MaxDataPoints,
|
||||
TimeRange: &pluginv2.TimeRange{
|
||||
ToEpochMS: query.TimeRange.GetToAsMsEpoch(),
|
||||
@@ -93,17 +73,17 @@ func (tw *DatasourcePluginWrapperV2) Query(ctx context.Context, ds *models.DataS
|
||||
|
||||
pbRes, err := tw.DataClient.QueryData(ctx, pbQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return DataResponse{}, err
|
||||
}
|
||||
|
||||
tR := &tsdb.Response{
|
||||
Results: make(map[string]*tsdb.QueryResult, len(pbRes.Responses)),
|
||||
tR := DataResponse{
|
||||
Results: make(map[string]DataQueryResult, len(pbRes.Responses)),
|
||||
}
|
||||
|
||||
for refID, pRes := range pbRes.Responses {
|
||||
qr := &tsdb.QueryResult{
|
||||
RefId: refID,
|
||||
Dataframes: tsdb.NewEncodedDataFrames(pRes.Frames),
|
||||
qr := DataQueryResult{
|
||||
RefID: refID,
|
||||
Dataframes: NewEncodedDataFrames(pRes.Frames),
|
||||
}
|
||||
if len(pRes.JsonMeta) != 0 {
|
||||
qr.Meta = simplejson.NewFromAny(pRes.JsonMeta)
|
||||
@@ -117,17 +97,3 @@ func (tw *DatasourcePluginWrapperV2) Query(ctx context.Context, ds *models.DataS
|
||||
|
||||
return tR, nil
|
||||
}
|
||||
|
||||
// BackendUserFromSignedInUser converts Grafana's SignedInUser model
|
||||
// to the backend plugin's model.
|
||||
func BackendUserFromSignedInUser(su *models.SignedInUser) *backend.User {
|
||||
if su == nil {
|
||||
return nil
|
||||
}
|
||||
return &backend.User{
|
||||
Login: su.Login,
|
||||
Name: su.Name,
|
||||
Email: su.Email,
|
||||
Role: string(su.OrgRole),
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
package plugins
|
||||
|
||||
const (
|
||||
signatureMissing ErrorCode = "signatureMissing"
|
||||
signatureModified ErrorCode = "signatureModified"
|
||||
signatureInvalid ErrorCode = "signatureInvalid"
|
||||
)
|
||||
|
||||
type ErrorCode string
|
||||
|
||||
type PluginError struct {
|
||||
|
||||
@@ -14,12 +14,15 @@ type FrontendPluginBase struct {
|
||||
PluginBase
|
||||
}
|
||||
|
||||
func (fp *FrontendPluginBase) initFrontendPlugin() {
|
||||
func (fp *FrontendPluginBase) InitFrontendPlugin() []*PluginStaticRoute {
|
||||
var staticRoutes []*PluginStaticRoute
|
||||
if isExternalPlugin(fp.PluginDir) {
|
||||
StaticRoutes = append(StaticRoutes, &PluginStaticRoute{
|
||||
Directory: fp.PluginDir,
|
||||
PluginId: fp.Id,
|
||||
})
|
||||
staticRoutes = []*PluginStaticRoute{
|
||||
{
|
||||
Directory: fp.PluginDir,
|
||||
PluginId: fp.Id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fp.handleModuleDefaults()
|
||||
@@ -30,6 +33,8 @@ func (fp *FrontendPluginBase) initFrontendPlugin() {
|
||||
for i := 0; i < len(fp.Info.Screenshots); i++ {
|
||||
fp.Info.Screenshots[i].Path = evalRelativePluginUrlPath(fp.Info.Screenshots[i].Path, fp.BaseUrl)
|
||||
}
|
||||
|
||||
return staticRoutes
|
||||
}
|
||||
|
||||
func getPluginLogoUrl(pluginType, path, baseUrl string) string {
|
||||
|
||||
@@ -1,31 +1,19 @@
|
||||
package plugins
|
||||
package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/tsdb/tsdbifaces"
|
||||
)
|
||||
|
||||
var varRegex = regexp.MustCompile(`(\$\{.+?\})`)
|
||||
|
||||
type ImportDashboardCommand struct {
|
||||
Dashboard *simplejson.Json
|
||||
Path string
|
||||
Inputs []ImportDashboardInput
|
||||
Overwrite bool
|
||||
FolderId int64
|
||||
|
||||
OrgId int64
|
||||
User *models.SignedInUser
|
||||
PluginId string
|
||||
Result *PluginDashboardInfoDTO
|
||||
}
|
||||
|
||||
type ImportDashboardInput struct {
|
||||
Type string `json:"type"`
|
||||
PluginId string `json:"pluginId"`
|
||||
@@ -41,58 +29,54 @@ func (e DashboardInputMissingError) Error() string {
|
||||
return fmt.Sprintf("Dashboard input variable: %v missing from import command", e.VariableName)
|
||||
}
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("plugins", ImportDashboard)
|
||||
}
|
||||
|
||||
func ImportDashboard(cmd *ImportDashboardCommand) error {
|
||||
func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json,
|
||||
overwrite bool, inputs []ImportDashboardInput, user *models.SignedInUser,
|
||||
requestHandler tsdbifaces.RequestHandler) (plugins.PluginDashboardInfoDTO, error) {
|
||||
var dashboard *models.Dashboard
|
||||
var err error
|
||||
|
||||
if cmd.PluginId != "" {
|
||||
if dashboard, err = loadPluginDashboard(cmd.PluginId, cmd.Path); err != nil {
|
||||
return err
|
||||
if pluginID != "" {
|
||||
var err error
|
||||
if dashboard, err = pm.LoadPluginDashboard(pluginID, path); err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, err
|
||||
}
|
||||
} else {
|
||||
dashboard = models.NewDashboardFromJson(cmd.Dashboard)
|
||||
dashboard = models.NewDashboardFromJson(dashboardModel)
|
||||
}
|
||||
|
||||
evaluator := &DashTemplateEvaluator{
|
||||
template: dashboard.Data,
|
||||
inputs: cmd.Inputs,
|
||||
inputs: inputs,
|
||||
}
|
||||
|
||||
generatedDash, err := evaluator.Eval()
|
||||
if err != nil {
|
||||
return err
|
||||
return plugins.PluginDashboardInfoDTO{}, err
|
||||
}
|
||||
|
||||
saveCmd := models.SaveDashboardCommand{
|
||||
Dashboard: generatedDash,
|
||||
OrgId: cmd.OrgId,
|
||||
UserId: cmd.User.UserId,
|
||||
Overwrite: cmd.Overwrite,
|
||||
PluginId: cmd.PluginId,
|
||||
FolderId: cmd.FolderId,
|
||||
OrgId: orgID,
|
||||
UserId: user.UserId,
|
||||
Overwrite: overwrite,
|
||||
PluginId: pluginID,
|
||||
FolderId: folderID,
|
||||
}
|
||||
|
||||
dto := &dashboards.SaveDashboardDTO{
|
||||
OrgId: cmd.OrgId,
|
||||
OrgId: orgID,
|
||||
Dashboard: saveCmd.GetDashboardModel(),
|
||||
Overwrite: saveCmd.Overwrite,
|
||||
User: cmd.User,
|
||||
User: user,
|
||||
}
|
||||
|
||||
savedDash, err := dashboards.NewService().ImportDashboard(dto)
|
||||
|
||||
savedDash, err := dashboards.NewService(requestHandler).ImportDashboard(dto)
|
||||
if err != nil {
|
||||
return err
|
||||
return plugins.PluginDashboardInfoDTO{}, err
|
||||
}
|
||||
|
||||
cmd.Result = &PluginDashboardInfoDTO{
|
||||
PluginId: cmd.PluginId,
|
||||
return plugins.PluginDashboardInfoDTO{
|
||||
PluginId: pluginID,
|
||||
Title: savedDash.Title,
|
||||
Path: cmd.Path,
|
||||
Path: path,
|
||||
Revision: savedDash.Data.Get("revision").MustInt64(1),
|
||||
FolderId: savedDash.FolderId,
|
||||
ImportedUri: "db/" + savedDash.Slug,
|
||||
@@ -101,9 +85,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
|
||||
Imported: true,
|
||||
DashboardId: savedDash.Id,
|
||||
Slug: savedDash.Slug,
|
||||
}
|
||||
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
type DashTemplateEvaluator struct {
|
||||
@@ -0,0 +1,97 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDashboardImport(t *testing.T) {
|
||||
pluginScenario(t, "When importing a plugin dashboard", func(t *testing.T, pm *PluginManager) {
|
||||
origNewDashboardService := dashboards.NewService
|
||||
t.Cleanup(func() {
|
||||
dashboards.NewService = origNewDashboardService
|
||||
})
|
||||
mock := &dashboards.FakeDashboardService{}
|
||||
dashboards.MockDashboardService(mock)
|
||||
|
||||
info, err := pm.ImportDashboard("test-app", "dashboards/connections.json", 1, 0, nil, false,
|
||||
[]ImportDashboardInput{
|
||||
{Name: "*", Type: "datasource", Value: "graphite"},
|
||||
}, &models.SignedInUser{UserId: 1, OrgRole: models.ROLE_ADMIN}, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
|
||||
resultStr, err := mock.SavedDashboards[0].Dashboard.Data.EncodePretty()
|
||||
require.NoError(t, err)
|
||||
expectedBytes, err := ioutil.ReadFile("testdata/test-app/dashboards/connections_result.json")
|
||||
require.NoError(t, err)
|
||||
expectedJson, err := simplejson.NewJson(expectedBytes)
|
||||
require.NoError(t, err)
|
||||
expectedStr, err := expectedJson.EncodePretty()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expectedStr, resultStr)
|
||||
|
||||
panel := mock.SavedDashboards[0].Dashboard.Data.Get("rows").GetIndex(0).Get("panels").GetIndex(0)
|
||||
require.Equal(t, "graphite", panel.Get("datasource").MustString())
|
||||
})
|
||||
|
||||
t.Run("When evaling dashboard template", func(t *testing.T) {
|
||||
template, err := simplejson.NewJson([]byte(`{
|
||||
"__inputs": [
|
||||
{
|
||||
"name": "DS_NAME",
|
||||
"type": "datasource"
|
||||
}
|
||||
],
|
||||
"test": {
|
||||
"prop": "${DS_NAME}_${DS_NAME}"
|
||||
}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
evaluator := &DashTemplateEvaluator{
|
||||
template: template,
|
||||
inputs: []ImportDashboardInput{
|
||||
{Name: "*", Type: "datasource", Value: "my-server"},
|
||||
},
|
||||
}
|
||||
|
||||
res, err := evaluator.Eval()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "my-server_my-server", res.GetPath("test", "prop").MustString())
|
||||
|
||||
inputs := res.Get("__inputs")
|
||||
require.Nil(t, inputs.Interface())
|
||||
})
|
||||
}
|
||||
|
||||
func pluginScenario(t *testing.T, desc string, fn func(*testing.T, *PluginManager)) {
|
||||
t.Helper()
|
||||
|
||||
t.Run("Given a plugin", func(t *testing.T) {
|
||||
pm := &PluginManager{
|
||||
Cfg: &setting.Cfg{
|
||||
FeatureToggles: map[string]bool{},
|
||||
PluginSettings: setting.PluginSettings{
|
||||
"test-app": map[string]string{
|
||||
"path": "testdata/test-app",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := pm.Init()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run(desc, func(t *testing.T) {
|
||||
fn(t, pm)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
func (pm *PluginManager) GetPluginDashboards(orgId int64, pluginId string) ([]*plugins.PluginDashboardInfoDTO, error) {
|
||||
plugin, exists := Plugins[pluginId]
|
||||
if !exists {
|
||||
return nil, plugins.PluginNotFoundError{PluginID: pluginId}
|
||||
}
|
||||
|
||||
result := make([]*plugins.PluginDashboardInfoDTO, 0)
|
||||
|
||||
// load current dashboards
|
||||
query := models.GetDashboardsByPluginIdQuery{OrgId: orgId, PluginId: pluginId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingMatches := make(map[int64]bool)
|
||||
for _, include := range plugin.Includes {
|
||||
if include.Type != plugins.PluginTypeDashboard {
|
||||
continue
|
||||
}
|
||||
|
||||
dashboard, err := pm.LoadPluginDashboard(plugin.Id, include.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &plugins.PluginDashboardInfoDTO{}
|
||||
res.Path = include.Path
|
||||
res.PluginId = plugin.Id
|
||||
res.Title = dashboard.Title
|
||||
res.Revision = dashboard.Data.Get("revision").MustInt64(1)
|
||||
|
||||
// find existing dashboard
|
||||
for _, existingDash := range query.Result {
|
||||
if existingDash.Slug == dashboard.Slug {
|
||||
res.DashboardId = existingDash.Id
|
||||
res.Imported = true
|
||||
res.ImportedUri = "db/" + existingDash.Slug
|
||||
res.ImportedUrl = existingDash.GetUrl()
|
||||
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
|
||||
existingMatches[existingDash.Id] = true
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, res)
|
||||
}
|
||||
|
||||
// find deleted dashboards
|
||||
for _, dash := range query.Result {
|
||||
if _, exists := existingMatches[dash.Id]; !exists {
|
||||
result = append(result, &plugins.PluginDashboardInfoDTO{
|
||||
Slug: dash.Slug,
|
||||
DashboardId: dash.Id,
|
||||
Removed: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (pm *PluginManager) LoadPluginDashboard(pluginId, path string) (*models.Dashboard, error) {
|
||||
plugin, exists := Plugins[pluginId]
|
||||
if !exists {
|
||||
return nil, plugins.PluginNotFoundError{PluginID: pluginId}
|
||||
}
|
||||
|
||||
dashboardFilePath := filepath.Join(plugin.PluginDir, path)
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based
|
||||
// on plugin folder structure on disk and not user input. `path` comes from the
|
||||
// `plugin.json` configuration file for the loaded plugin
|
||||
reader, err := os.Open(dashboardFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := reader.Close(); err != nil {
|
||||
plog.Warn("Failed to close file", "path", dashboardFilePath, "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
data, err := simplejson.NewFromReader(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return models.NewDashboardFromJson(data), nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package plugins
|
||||
package manager
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func TestPluginDashboards(t *testing.T) {
|
||||
Convey("When asking plugin dashboard info", t, func() {
|
||||
Convey("When asking for plugin dashboard info", t, func() {
|
||||
pm := &PluginManager{
|
||||
Cfg: &setting.Cfg{
|
||||
FeatureToggles: map[string]bool{},
|
||||
@@ -47,8 +47,7 @@ func TestPluginDashboards(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
dashboards, err := GetPluginDashboards(1, "test-app")
|
||||
|
||||
dashboards, err := pm.GetPluginDashboards(1, "test-app")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("should return 2 dashboards", func() {
|
||||
@@ -0,0 +1,11 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
const (
|
||||
signatureMissing plugins.ErrorCode = "signatureMissing"
|
||||
signatureModified plugins.ErrorCode = "signatureModified"
|
||||
signatureInvalid plugins.ErrorCode = "signatureInvalid"
|
||||
)
|
||||
@@ -0,0 +1,616 @@
|
||||
// Package manager contains plugin manager logic.
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
var (
|
||||
DataSources map[string]*plugins.DataSourcePlugin
|
||||
Panels map[string]*plugins.PanelPlugin
|
||||
StaticRoutes []*plugins.PluginStaticRoute
|
||||
Apps map[string]*plugins.AppPlugin
|
||||
Plugins map[string]*plugins.PluginBase
|
||||
PluginTypes map[string]interface{}
|
||||
Renderer *plugins.RendererPlugin
|
||||
|
||||
plog log.Logger
|
||||
)
|
||||
|
||||
type unsignedPluginConditionFunc = func(plugin *plugins.PluginBase) bool
|
||||
|
||||
type PluginScanner struct {
|
||||
pluginPath string
|
||||
errors []error
|
||||
backendPluginManager backendplugin.Manager
|
||||
cfg *setting.Cfg
|
||||
requireSigned bool
|
||||
log log.Logger
|
||||
plugins map[string]*plugins.PluginBase
|
||||
allowUnsignedPluginsCondition unsignedPluginConditionFunc
|
||||
}
|
||||
|
||||
type PluginManager struct {
|
||||
BackendPluginManager backendplugin.Manager `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
log log.Logger
|
||||
scanningErrors []error
|
||||
|
||||
// AllowUnsignedPluginsCondition changes the policy for allowing unsigned plugins. Signature validation only runs when plugins are starting
|
||||
// and running plugins will not be terminated if they violate the new policy.
|
||||
AllowUnsignedPluginsCondition unsignedPluginConditionFunc
|
||||
GrafanaLatestVersion string
|
||||
GrafanaHasUpdate bool
|
||||
pluginScanningErrors map[string]plugins.PluginError
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.RegisterService(&PluginManager{})
|
||||
}
|
||||
|
||||
func (pm *PluginManager) Init() error {
|
||||
pm.log = log.New("plugins")
|
||||
plog = log.New("plugins")
|
||||
|
||||
DataSources = map[string]*plugins.DataSourcePlugin{}
|
||||
StaticRoutes = []*plugins.PluginStaticRoute{}
|
||||
Panels = map[string]*plugins.PanelPlugin{}
|
||||
Apps = map[string]*plugins.AppPlugin{}
|
||||
Plugins = map[string]*plugins.PluginBase{}
|
||||
PluginTypes = map[string]interface{}{
|
||||
"panel": plugins.PanelPlugin{},
|
||||
"datasource": plugins.DataSourcePlugin{},
|
||||
"app": plugins.AppPlugin{},
|
||||
"renderer": plugins.RendererPlugin{},
|
||||
}
|
||||
pm.pluginScanningErrors = map[string]plugins.PluginError{}
|
||||
|
||||
pm.log.Info("Starting plugin search")
|
||||
|
||||
plugDir := filepath.Join(pm.Cfg.StaticRootPath, "app/plugins")
|
||||
pm.log.Debug("Scanning core plugin directory", "dir", plugDir)
|
||||
if err := pm.scan(plugDir, false); err != nil {
|
||||
return errutil.Wrapf(err, "failed to scan core plugin directory '%s'", plugDir)
|
||||
}
|
||||
|
||||
plugDir = pm.Cfg.BundledPluginsPath
|
||||
pm.log.Debug("Scanning bundled plugins directory", "dir", plugDir)
|
||||
exists, err := fs.Exists(plugDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
if err := pm.scan(plugDir, false); err != nil {
|
||||
return errutil.Wrapf(err, "failed to scan bundled plugins directory '%s'", plugDir)
|
||||
}
|
||||
}
|
||||
|
||||
// check if plugins dir exists
|
||||
exists, err = fs.Exists(pm.Cfg.PluginsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if err = os.MkdirAll(pm.Cfg.PluginsPath, os.ModePerm); err != nil {
|
||||
pm.log.Error("failed to create external plugins directory", "dir", pm.Cfg.PluginsPath, "error", err)
|
||||
} else {
|
||||
pm.log.Info("External plugins directory created", "directory", pm.Cfg.PluginsPath)
|
||||
}
|
||||
} else {
|
||||
pm.log.Debug("Scanning external plugins directory", "dir", pm.Cfg.PluginsPath)
|
||||
if err := pm.scan(pm.Cfg.PluginsPath, true); err != nil {
|
||||
return errutil.Wrapf(err, "failed to scan external plugins directory '%s'",
|
||||
pm.Cfg.PluginsPath)
|
||||
}
|
||||
}
|
||||
|
||||
if err := pm.scanPluginPaths(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, panel := range Panels {
|
||||
staticRoutes := panel.InitFrontendPlugin()
|
||||
StaticRoutes = append(StaticRoutes, staticRoutes...)
|
||||
}
|
||||
|
||||
for _, ds := range DataSources {
|
||||
staticRoutes := ds.InitFrontendPlugin()
|
||||
StaticRoutes = append(StaticRoutes, staticRoutes...)
|
||||
}
|
||||
|
||||
for _, app := range Apps {
|
||||
staticRoutes := app.InitApp(Panels, DataSources)
|
||||
StaticRoutes = append(StaticRoutes, staticRoutes...)
|
||||
}
|
||||
|
||||
if Renderer != nil {
|
||||
staticRoutes := Renderer.InitFrontendPlugin()
|
||||
StaticRoutes = append(StaticRoutes, staticRoutes...)
|
||||
}
|
||||
|
||||
for _, p := range Plugins {
|
||||
if p.IsCorePlugin {
|
||||
p.Signature = plugins.PluginSignatureInternal
|
||||
} else {
|
||||
metrics.SetPluginBuildInformation(p.Id, p.Type, p.Info.Version)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *PluginManager) Run(ctx context.Context) error {
|
||||
pm.checkForUpdates()
|
||||
|
||||
ticker := time.NewTicker(time.Minute * 10)
|
||||
run := true
|
||||
|
||||
for run {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
pm.checkForUpdates()
|
||||
case <-ctx.Done():
|
||||
run = false
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// scanPluginPaths scans configured plugin paths.
|
||||
func (pm *PluginManager) scanPluginPaths() error {
|
||||
for pluginID, settings := range pm.Cfg.PluginSettings {
|
||||
path, exists := settings["path"]
|
||||
if !exists || path == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := pm.scan(path, true); err != nil {
|
||||
return errutil.Wrapf(err, "failed to scan directory configured for plugin '%s': '%s'", pluginID, path)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scan a directory for plugins.
|
||||
func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error {
|
||||
scanner := &PluginScanner{
|
||||
pluginPath: pluginDir,
|
||||
backendPluginManager: pm.BackendPluginManager,
|
||||
cfg: pm.Cfg,
|
||||
requireSigned: requireSigned,
|
||||
log: pm.log,
|
||||
plugins: map[string]*plugins.PluginBase{},
|
||||
allowUnsignedPluginsCondition: pm.AllowUnsignedPluginsCondition,
|
||||
}
|
||||
|
||||
// 1st pass: Scan plugins, also mapping plugins to their respective directories
|
||||
if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
pm.log.Debug("Couldn't scan directory since it doesn't exist", "pluginDir", pluginDir, "err", err)
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
pm.log.Debug("Couldn't scan directory due to lack of permissions", "pluginDir", pluginDir, "err", err)
|
||||
return nil
|
||||
}
|
||||
if pluginDir != "data/plugins" {
|
||||
pm.log.Warn("Could not scan dir", "pluginDir", pluginDir, "err", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
pm.log.Debug("Initial plugin loading done")
|
||||
|
||||
// 2nd pass: Validate and register plugins
|
||||
for dpath, plugin := range scanner.plugins {
|
||||
// Try to find any root plugin
|
||||
ancestors := strings.Split(dpath, string(filepath.Separator))
|
||||
ancestors = ancestors[0 : len(ancestors)-1]
|
||||
aPath := ""
|
||||
if runtime.GOOS != "windows" && filepath.IsAbs(dpath) {
|
||||
aPath = "/"
|
||||
}
|
||||
for _, a := range ancestors {
|
||||
aPath = filepath.Join(aPath, a)
|
||||
if root, ok := scanner.plugins[aPath]; ok {
|
||||
plugin.Root = root
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pm.log.Debug("Found plugin", "id", plugin.Id, "signature", plugin.Signature, "hasRoot", plugin.Root != nil)
|
||||
signingError := scanner.validateSignature(plugin)
|
||||
if signingError != nil {
|
||||
pm.log.Debug("Failed to validate plugin signature. Will skip loading", "id", plugin.Id,
|
||||
"signature", plugin.Signature, "status", signingError.ErrorCode)
|
||||
pm.pluginScanningErrors[plugin.Id] = *signingError
|
||||
continue
|
||||
}
|
||||
|
||||
pm.log.Debug("Attempting to add plugin", "id", plugin.Id)
|
||||
|
||||
pluginGoType, exists := PluginTypes[plugin.Type]
|
||||
if !exists {
|
||||
return fmt.Errorf("unknown plugin type %q", plugin.Type)
|
||||
}
|
||||
|
||||
jsonFPath := filepath.Join(plugin.PluginDir, "plugin.json")
|
||||
|
||||
// External plugins need a module.js file for SystemJS to load
|
||||
if !strings.HasPrefix(jsonFPath, pm.Cfg.StaticRootPath) && !scanner.IsBackendOnlyPlugin(plugin.Type) {
|
||||
module := filepath.Join(plugin.PluginDir, "module.js")
|
||||
exists, err := fs.Exists(module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
scanner.log.Warn("Plugin missing module.js",
|
||||
"name", plugin.Name,
|
||||
"warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.",
|
||||
"path", module)
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `jsonFPath` is based
|
||||
// on plugin the folder structure on disk and not user input.
|
||||
reader, err := os.Open(jsonFPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := reader.Close(); err != nil {
|
||||
scanner.log.Warn("Failed to close JSON file", "path", jsonFPath, "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
jsonParser := json.NewDecoder(reader)
|
||||
|
||||
loader := reflect.New(reflect.TypeOf(pluginGoType)).Interface().(plugins.PluginLoader)
|
||||
|
||||
// Load the full plugin, and add it to manager
|
||||
if err := pm.loadPlugin(jsonParser, plugin, scanner, loader); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(scanner.errors) > 0 {
|
||||
pm.log.Warn("Some plugins failed to load", "errors", scanner.errors)
|
||||
pm.scanningErrors = scanner.errors
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *PluginManager) loadPlugin(jsonParser *json.Decoder, pluginBase *plugins.PluginBase,
|
||||
scanner *PluginScanner, loader plugins.PluginLoader) error {
|
||||
plug, err := loader.Load(jsonParser, pluginBase, scanner.backendPluginManager)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pb *plugins.PluginBase
|
||||
switch p := plug.(type) {
|
||||
case *plugins.DataSourcePlugin:
|
||||
DataSources[p.Id] = p
|
||||
pb = &p.PluginBase
|
||||
case *plugins.PanelPlugin:
|
||||
Panels[p.Id] = p
|
||||
pb = &p.PluginBase
|
||||
case *plugins.RendererPlugin:
|
||||
Renderer = p
|
||||
pb = &p.PluginBase
|
||||
case *plugins.AppPlugin:
|
||||
Apps[p.Id] = p
|
||||
pb = &p.PluginBase
|
||||
default:
|
||||
panic(fmt.Sprintf("Unrecognized plugin type %T", plug))
|
||||
}
|
||||
|
||||
if p, exists := Plugins[pb.Id]; exists {
|
||||
pm.log.Warn("Plugin is duplicate", "id", pb.Id)
|
||||
scanner.errors = append(scanner.errors, plugins.DuplicatePluginError{Plugin: pb, ExistingPlugin: p})
|
||||
return nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(pluginBase.PluginDir, pm.Cfg.StaticRootPath) {
|
||||
pm.log.Info("Registering plugin", "id", pb.Id)
|
||||
}
|
||||
|
||||
if len(pb.Dependencies.Plugins) == 0 {
|
||||
pb.Dependencies.Plugins = []plugins.PluginDependencyItem{}
|
||||
}
|
||||
|
||||
if pb.Dependencies.GrafanaVersion == "" {
|
||||
pb.Dependencies.GrafanaVersion = "*"
|
||||
}
|
||||
|
||||
for _, include := range pb.Includes {
|
||||
if include.Role == "" {
|
||||
include.Role = models.ROLE_VIEWER
|
||||
}
|
||||
}
|
||||
|
||||
// Copy relevant fields from the base
|
||||
pb.PluginDir = pluginBase.PluginDir
|
||||
pb.Signature = pluginBase.Signature
|
||||
pb.SignatureType = pluginBase.SignatureType
|
||||
pb.SignatureOrg = pluginBase.SignatureOrg
|
||||
|
||||
Plugins[pb.Id] = pb
|
||||
pm.log.Debug("Successfully added plugin", "id", pb.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDatasource returns a datasource based on passed pluginID if it exists
|
||||
//
|
||||
// This function fetches the datasource from the global variable DataSources in this package.
|
||||
// Rather then refactor all dependencies on the global variable we can use this as an transition.
|
||||
func (pm *PluginManager) GetDatasource(pluginID string) (*plugins.DataSourcePlugin, bool) {
|
||||
ds, exists := DataSources[pluginID]
|
||||
return ds, exists
|
||||
}
|
||||
|
||||
func (s *PluginScanner) walker(currentPath string, f os.FileInfo, err error) error {
|
||||
// We scan all the subfolders for plugin.json (with some exceptions) so that we also load embedded plugins, for
|
||||
// example https://github.com/raintank/worldping-app/tree/master/dist/grafana-worldmap-panel worldmap panel plugin
|
||||
// is embedded in worldping app.
|
||||
if err != nil {
|
||||
return fmt.Errorf("filepath.Walk reported an error for %q: %w", currentPath, err)
|
||||
}
|
||||
|
||||
if f.Name() == "node_modules" || f.Name() == "Chromium.app" {
|
||||
return util.ErrWalkSkipDir
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if f.Name() != "plugin.json" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.loadPlugin(currentPath); err != nil {
|
||||
s.log.Error("Failed to load plugin", "error", err, "pluginPath", filepath.Dir(currentPath))
|
||||
s.errors = append(s.errors, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PluginScanner) loadPlugin(pluginJSONFilePath string) error {
|
||||
s.log.Debug("Loading plugin", "path", pluginJSONFilePath)
|
||||
currentDir := filepath.Dir(pluginJSONFilePath)
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `currentPath` is based
|
||||
// on plugin the folder structure on disk and not user input.
|
||||
reader, err := os.Open(pluginJSONFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := reader.Close(); err != nil {
|
||||
s.log.Warn("Failed to close JSON file", "path", pluginJSONFilePath, "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
jsonParser := json.NewDecoder(reader)
|
||||
pluginCommon := plugins.PluginBase{}
|
||||
if err := jsonParser.Decode(&pluginCommon); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pluginCommon.Id == "" || pluginCommon.Type == "" {
|
||||
return errors.New("did not find type or id properties in plugin.json")
|
||||
}
|
||||
|
||||
pluginCommon.PluginDir = filepath.Dir(pluginJSONFilePath)
|
||||
pluginCommon.Files, err = collectPluginFilesWithin(pluginCommon.PluginDir)
|
||||
if err != nil {
|
||||
s.log.Warn("Could not collect plugin file information in directory", "pluginID", pluginCommon.Id, "dir", pluginCommon.PluginDir)
|
||||
return err
|
||||
}
|
||||
|
||||
signatureState, err := getPluginSignatureState(s.log, &pluginCommon)
|
||||
if err != nil {
|
||||
s.log.Warn("Could not get plugin signature state", "pluginID", pluginCommon.Id, "err", err)
|
||||
return err
|
||||
}
|
||||
pluginCommon.Signature = signatureState.Status
|
||||
pluginCommon.SignatureType = signatureState.Type
|
||||
pluginCommon.SignatureOrg = signatureState.SigningOrg
|
||||
|
||||
s.plugins[currentDir] = &pluginCommon
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*PluginScanner) IsBackendOnlyPlugin(pluginType string) bool {
|
||||
return pluginType == "renderer"
|
||||
}
|
||||
|
||||
// validateSignature validates a plugin's signature.
|
||||
func (s *PluginScanner) validateSignature(plugin *plugins.PluginBase) *plugins.PluginError {
|
||||
if plugin.Signature == plugins.PluginSignatureValid {
|
||||
s.log.Debug("Plugin has valid signature", "id", plugin.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
if plugin.Root != nil {
|
||||
// If a descendant plugin with invalid signature, set signature to that of root
|
||||
if plugin.IsCorePlugin || plugin.Signature == plugins.PluginSignatureInternal {
|
||||
s.log.Debug("Not setting descendant plugin's signature to that of root since it's core or internal",
|
||||
"plugin", plugin.Id, "signature", plugin.Signature, "isCore", plugin.IsCorePlugin)
|
||||
} else {
|
||||
s.log.Debug("Setting descendant plugin's signature to that of root", "plugin", plugin.Id,
|
||||
"root", plugin.Root.Id, "signature", plugin.Signature, "rootSignature", plugin.Root.Signature)
|
||||
plugin.Signature = plugin.Root.Signature
|
||||
if plugin.Signature == plugins.PluginSignatureValid {
|
||||
s.log.Debug("Plugin has valid signature (inherited from root)", "id", plugin.Id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.log.Debug("Non-valid plugin Signature", "pluginID", plugin.Id, "pluginDir", plugin.PluginDir,
|
||||
"state", plugin.Signature)
|
||||
}
|
||||
|
||||
// For the time being, we choose to only require back-end plugins to be signed
|
||||
// NOTE: the state is calculated again when setting metadata on the object
|
||||
if !plugin.Backend || !s.requireSigned {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch plugin.Signature {
|
||||
case plugins.PluginSignatureUnsigned:
|
||||
if allowed := s.allowUnsigned(plugin); !allowed {
|
||||
s.log.Debug("Plugin is unsigned", "id", plugin.Id)
|
||||
s.errors = append(s.errors, fmt.Errorf("plugin %q is unsigned", plugin.Id))
|
||||
return &plugins.PluginError{
|
||||
ErrorCode: signatureMissing,
|
||||
}
|
||||
}
|
||||
s.log.Warn("Running an unsigned backend plugin", "pluginID", plugin.Id, "pluginDir",
|
||||
plugin.PluginDir)
|
||||
return nil
|
||||
case plugins.PluginSignatureInvalid:
|
||||
s.log.Debug("Plugin %q has an invalid signature", plugin.Id)
|
||||
s.errors = append(s.errors, fmt.Errorf("plugin %q has an invalid signature", plugin.Id))
|
||||
return &plugins.PluginError{
|
||||
ErrorCode: signatureInvalid,
|
||||
}
|
||||
case plugins.PluginSignatureModified:
|
||||
s.log.Debug("Plugin %q has a modified signature", plugin.Id)
|
||||
s.errors = append(s.errors, fmt.Errorf("plugin %q's signature has been modified", plugin.Id))
|
||||
return &plugins.PluginError{
|
||||
ErrorCode: signatureModified,
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("Plugin %q has unrecognized plugin signature state %q", plugin.Id, plugin.Signature))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PluginScanner) allowUnsigned(plugin *plugins.PluginBase) bool {
|
||||
if s.allowUnsignedPluginsCondition != nil {
|
||||
return s.allowUnsignedPluginsCondition(plugin)
|
||||
}
|
||||
|
||||
if s.cfg.Env == setting.Dev {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, plug := range s.cfg.PluginsAllowUnsigned {
|
||||
if plug == plugin.Id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ScanningErrors returns plugin scanning errors encountered.
|
||||
func (pm *PluginManager) ScanningErrors() []plugins.PluginError {
|
||||
scanningErrs := make([]plugins.PluginError, 0)
|
||||
for id, e := range pm.pluginScanningErrors {
|
||||
scanningErrs = append(scanningErrs, plugins.PluginError{
|
||||
ErrorCode: e.ErrorCode,
|
||||
PluginID: id,
|
||||
})
|
||||
}
|
||||
return scanningErrs
|
||||
}
|
||||
|
||||
func (pm *PluginManager) GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
|
||||
plug, exists := Plugins[pluginId]
|
||||
if !exists {
|
||||
return nil, plugins.PluginNotFoundError{PluginID: pluginId}
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `plug.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)))
|
||||
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)))
|
||||
}
|
||||
|
||||
exists, err = fs.Exists(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return make([]byte, 0), nil
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based
|
||||
// on plugin the folder structure on disk and not user input.
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// gets plugin filenames that require verification for plugin signing
|
||||
func collectPluginFilesWithin(rootDir string) ([]string, error) {
|
||||
var files []string
|
||||
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() && info.Name() != "MANIFEST.txt" {
|
||||
file, err := filepath.Rel(rootDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files = append(files, filepath.ToSlash(file))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return files, err
|
||||
}
|
||||
|
||||
// GetDataPlugin gets a DataPlugin with a certain name. If none is found, nil is returned.
|
||||
func (pm *PluginManager) GetDataPlugin(pluginID string) plugins.DataPlugin {
|
||||
if p, exists := DataSources[pluginID]; exists && p.CanHandleDataQueries() {
|
||||
return p
|
||||
}
|
||||
|
||||
// XXX: Might other plugins implement DataPlugin?
|
||||
|
||||
p := pm.BackendPluginManager.GetDataPlugin(pluginID)
|
||||
if p != nil {
|
||||
return p.(plugins.DataPlugin)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package plugins
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
func TestPluginManager_Init(t *testing.T) {
|
||||
staticRootPath, err := filepath.Abs("../../public/")
|
||||
staticRootPath, err := filepath.Abs("../../../public/")
|
||||
require.NoError(t, err)
|
||||
|
||||
origRootPath := setting.StaticRootPath
|
||||
@@ -132,7 +133,7 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, pm.scanningErrors, 1)
|
||||
assert.True(t, errors.Is(pm.scanningErrors[0], duplicatePluginError{}))
|
||||
assert.True(t, errors.Is(pm.scanningErrors[0], plugins.DuplicatePluginError{}))
|
||||
})
|
||||
|
||||
t.Run("With external back-end plugin with valid v2 signature", func(t *testing.T) {
|
||||
@@ -152,8 +153,8 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
assert.Equal(t, "Test", Plugins[pluginId].Name)
|
||||
assert.Equal(t, pluginId, Plugins[pluginId].Id)
|
||||
assert.Equal(t, "1.0.0", Plugins[pluginId].Info.Version)
|
||||
assert.Equal(t, pluginSignatureValid, Plugins[pluginId].Signature)
|
||||
assert.Equal(t, grafanaType, Plugins[pluginId].SignatureType)
|
||||
assert.Equal(t, plugins.PluginSignatureValid, Plugins[pluginId].Signature)
|
||||
assert.Equal(t, plugins.GrafanaType, Plugins[pluginId].SignatureType)
|
||||
assert.Equal(t, "Grafana Labs", Plugins[pluginId].SignatureOrg)
|
||||
assert.False(t, Plugins[pluginId].IsCorePlugin)
|
||||
})
|
||||
@@ -200,8 +201,8 @@ func TestPluginManager_Init(t *testing.T) {
|
||||
assert.Equal(t, "Test", Plugins[pluginId].Name)
|
||||
assert.Equal(t, pluginId, Plugins[pluginId].Id)
|
||||
assert.Equal(t, "1.0.0", Plugins[pluginId].Info.Version)
|
||||
assert.Equal(t, pluginSignatureValid, Plugins[pluginId].Signature)
|
||||
assert.Equal(t, privateType, Plugins[pluginId].SignatureType)
|
||||
assert.Equal(t, plugins.PluginSignatureValid, Plugins[pluginId].Signature)
|
||||
assert.Equal(t, plugins.PrivateType, Plugins[pluginId].SignatureType)
|
||||
assert.Equal(t, "Will Browne", Plugins[pluginId].SignatureOrg)
|
||||
assert.False(t, Plugins[pluginId].IsCorePlugin)
|
||||
})
|
||||
@@ -266,6 +267,8 @@ func TestPluginManager_IsBackendOnlyPlugin(t *testing.T) {
|
||||
}
|
||||
|
||||
type fakeBackendPluginManager struct {
|
||||
backendplugin.Manager
|
||||
|
||||
registeredPlugins []string
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package plugins
|
||||
package manager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
|
||||
@@ -56,11 +57,11 @@ type pluginManifest struct {
|
||||
Files map[string]string `json:"files"`
|
||||
|
||||
// V2 supported fields
|
||||
ManifestVersion string `json:"manifestVersion"`
|
||||
SignatureType PluginSignatureType `json:"signatureType"`
|
||||
SignedByOrg string `json:"signedByOrg"`
|
||||
SignedByOrgName string `json:"signedByOrgName"`
|
||||
RootURLs []string `json:"rootUrls"`
|
||||
ManifestVersion string `json:"manifestVersion"`
|
||||
SignatureType plugins.PluginSignatureType `json:"signatureType"`
|
||||
SignedByOrg string `json:"signedByOrg"`
|
||||
SignedByOrgName string `json:"signedByOrgName"`
|
||||
RootURLs []string `json:"rootUrls"`
|
||||
}
|
||||
|
||||
func (m *pluginManifest) isV2() bool {
|
||||
@@ -97,7 +98,7 @@ func readPluginManifest(body []byte) (*pluginManifest, error) {
|
||||
}
|
||||
|
||||
// getPluginSignatureState returns the signature state for a plugin.
|
||||
func getPluginSignatureState(log log.Logger, plugin *PluginBase) (PluginSignatureState, error) {
|
||||
func getPluginSignatureState(log log.Logger, plugin *plugins.PluginBase) (plugins.PluginSignatureState, error) {
|
||||
log.Debug("Getting signature state of plugin", "plugin", plugin.Id, "isBackend", plugin.Backend)
|
||||
manifestPath := filepath.Join(plugin.PluginDir, "MANIFEST.txt")
|
||||
|
||||
@@ -107,31 +108,31 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) (PluginSignatur
|
||||
byteValue, err := ioutil.ReadFile(manifestPath)
|
||||
if err != nil || len(byteValue) < 10 {
|
||||
log.Debug("Plugin is unsigned", "id", plugin.Id)
|
||||
return PluginSignatureState{
|
||||
Status: pluginSignatureUnsigned,
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureUnsigned,
|
||||
}, nil
|
||||
}
|
||||
|
||||
manifest, err := readPluginManifest(byteValue)
|
||||
if err != nil {
|
||||
log.Debug("Plugin signature invalid", "id", plugin.Id)
|
||||
return PluginSignatureState{
|
||||
Status: pluginSignatureInvalid,
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureInvalid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Make sure the versions all match
|
||||
if manifest.Plugin != plugin.Id || manifest.Version != plugin.Info.Version {
|
||||
return PluginSignatureState{
|
||||
Status: pluginSignatureModified,
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureModified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate that private is running within defined root URLs
|
||||
if manifest.SignatureType == privateType {
|
||||
if manifest.SignatureType == plugins.PrivateType {
|
||||
appURL, err := url.Parse(setting.AppUrl)
|
||||
if err != nil {
|
||||
return PluginSignatureState{}, err
|
||||
return plugins.PluginSignatureState{}, err
|
||||
}
|
||||
|
||||
foundMatch := false
|
||||
@@ -139,7 +140,7 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) (PluginSignatur
|
||||
rootURL, err := url.Parse(u)
|
||||
if err != nil {
|
||||
log.Warn("Could not parse plugin root URL", "plugin", plugin.Id, "rootUrl", rootURL)
|
||||
return PluginSignatureState{}, err
|
||||
return plugins.PluginSignatureState{}, err
|
||||
}
|
||||
if rootURL.Scheme == appURL.Scheme &&
|
||||
rootURL.Host == appURL.Host &&
|
||||
@@ -150,9 +151,10 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) (PluginSignatur
|
||||
}
|
||||
|
||||
if !foundMatch {
|
||||
log.Warn("Could not find root URL that matches running application URL", "plugin", plugin.Id, "appUrl", appURL, "rootUrls", manifest.RootURLs)
|
||||
return PluginSignatureState{
|
||||
Status: pluginSignatureInvalid,
|
||||
log.Warn("Could not find root URL that matches running application URL", "plugin", plugin.Id,
|
||||
"appUrl", appURL, "rootUrls", manifest.RootURLs)
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureInvalid,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -171,8 +173,8 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) (PluginSignatur
|
||||
f, err := os.Open(fp)
|
||||
if err != nil {
|
||||
log.Warn("Plugin file listed in the manifest was not found", "plugin", plugin.Id, "filename", p, "dir", plugin.PluginDir)
|
||||
return PluginSignatureState{
|
||||
Status: pluginSignatureModified,
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureModified,
|
||||
}, nil
|
||||
}
|
||||
defer func() {
|
||||
@@ -184,15 +186,15 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) (PluginSignatur
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
log.Warn("Couldn't read plugin file", "plugin", plugin.Id, "filename", fp)
|
||||
return PluginSignatureState{
|
||||
Status: pluginSignatureModified,
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureModified,
|
||||
}, nil
|
||||
}
|
||||
sum := hex.EncodeToString(h.Sum(nil))
|
||||
if sum != hash {
|
||||
log.Warn("Plugin file's signature has been modified versus manifest", "plugin", plugin.Id, "filename", fp)
|
||||
return PluginSignatureState{
|
||||
Status: pluginSignatureModified,
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureModified,
|
||||
}, nil
|
||||
}
|
||||
manifestFiles[p] = true
|
||||
@@ -209,16 +211,16 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) (PluginSignatur
|
||||
|
||||
if len(unsignedFiles) > 0 {
|
||||
log.Warn("The following files were not included in the signature", "plugin", plugin.Id, "files", unsignedFiles)
|
||||
return PluginSignatureState{
|
||||
Status: pluginSignatureModified,
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureModified,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Everything OK
|
||||
log.Debug("Plugin signature valid", "id", plugin.Id)
|
||||
return PluginSignatureState{
|
||||
Status: pluginSignatureValid,
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureValid,
|
||||
Type: manifest.SignatureType,
|
||||
SigningOrg: manifest.SignedByOrgName,
|
||||
}, nil
|
||||
@@ -1,10 +1,11 @@
|
||||
package plugins
|
||||
package manager
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -104,7 +105,7 @@ khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
|
||||
assert.Equal(t, int64(1605807018050), manifest.Time)
|
||||
assert.Equal(t, "7e4d0c6a708866e7", manifest.KeyID)
|
||||
assert.Equal(t, "2.0.0", manifest.ManifestVersion)
|
||||
assert.Equal(t, privateType, manifest.SignatureType)
|
||||
assert.Equal(t, plugins.PrivateType, manifest.SignatureType)
|
||||
assert.Equal(t, "willbrowne", manifest.SignedByOrg)
|
||||
assert.Equal(t, "Will Browne", manifest.SignedByOrgName)
|
||||
assert.Equal(t, []string{"http://localhost:3000/"}, manifest.RootURLs)
|
||||
@@ -1,13 +1,13 @@
|
||||
package plugins
|
||||
package manager
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
func GetPluginSettings(orgId int64) (map[string]*models.PluginSettingInfoDTO, error) {
|
||||
func (pm *PluginManager) GetPluginSettings(orgId int64) (map[string]*models.PluginSettingInfoDTO, error) {
|
||||
query := models.GetPluginSettingsQuery{OrgId: orgId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -52,39 +52,39 @@ func GetPluginSettings(orgId int64) (map[string]*models.PluginSettingInfoDTO, er
|
||||
return pluginMap, nil
|
||||
}
|
||||
|
||||
func GetEnabledPlugins(orgId int64) (*EnabledPlugins, error) {
|
||||
enabledPlugins := NewEnabledPlugins()
|
||||
pluginSettingMap, err := GetPluginSettings(orgId)
|
||||
func (pm *PluginManager) GetEnabledPlugins(orgID int64) (*plugins.EnabledPlugins, error) {
|
||||
enabledPlugins := &plugins.EnabledPlugins{
|
||||
Panels: make([]*plugins.PanelPlugin, 0),
|
||||
DataSources: make(map[string]*plugins.DataSourcePlugin),
|
||||
Apps: make([]*plugins.AppPlugin, 0),
|
||||
}
|
||||
|
||||
pluginSettingMap, err := pm.GetPluginSettings(orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return enabledPlugins, err
|
||||
}
|
||||
|
||||
isPluginEnabled := func(pluginId string) bool {
|
||||
_, ok := pluginSettingMap[pluginId]
|
||||
return ok
|
||||
}
|
||||
|
||||
for pluginId, app := range Apps {
|
||||
if b, ok := pluginSettingMap[pluginId]; ok {
|
||||
for pluginID, app := range Apps {
|
||||
if b, ok := pluginSettingMap[pluginID]; ok {
|
||||
app.Pinned = b.Pinned
|
||||
enabledPlugins.Apps = append(enabledPlugins.Apps, app)
|
||||
}
|
||||
}
|
||||
|
||||
// add all plugins that are not part of an App.
|
||||
for dsId, ds := range DataSources {
|
||||
if isPluginEnabled(ds.Id) {
|
||||
enabledPlugins.DataSources[dsId] = ds
|
||||
for dsID, ds := range DataSources {
|
||||
if _, exists := pluginSettingMap[ds.Id]; exists {
|
||||
enabledPlugins.DataSources[dsID] = ds
|
||||
}
|
||||
}
|
||||
|
||||
for _, panel := range Panels {
|
||||
if isPluginEnabled(panel.Id) {
|
||||
if _, exists := pluginSettingMap[panel.Id]; exists {
|
||||
enabledPlugins.Panels = append(enabledPlugins.Panels, panel)
|
||||
}
|
||||
}
|
||||
|
||||
return &enabledPlugins, nil
|
||||
return enabledPlugins, nil
|
||||
}
|
||||
|
||||
// IsAppInstalled checks if an app plugin with provided plugin ID is installed.
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Vendored
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
package plugins
|
||||
package manager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -40,7 +40,7 @@ func getAllExternalPluginSlugs() string {
|
||||
}
|
||||
|
||||
func (pm *PluginManager) checkForUpdates() {
|
||||
if !setting.CheckForUpdates {
|
||||
if !pm.Cfg.CheckForUpdates {
|
||||
return
|
||||
}
|
||||
|
||||
+7
-87
@@ -3,55 +3,16 @@ package plugins
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var (
|
||||
const (
|
||||
PluginTypeApp = "app"
|
||||
PluginTypeDashboard = "dashboard"
|
||||
)
|
||||
|
||||
type PluginState string
|
||||
|
||||
var (
|
||||
PluginStateAlpha PluginState = "alpha"
|
||||
)
|
||||
|
||||
type PluginSignatureState struct {
|
||||
Status PluginSignatureStatus
|
||||
Type PluginSignatureType
|
||||
SigningOrg string
|
||||
}
|
||||
|
||||
type PluginSignatureStatus string
|
||||
|
||||
func (pss PluginSignatureStatus) IsValid() bool {
|
||||
return pss == pluginSignatureValid
|
||||
}
|
||||
|
||||
func (pss PluginSignatureStatus) IsInternal() bool {
|
||||
return pss == pluginSignatureInternal
|
||||
}
|
||||
|
||||
const (
|
||||
pluginSignatureInternal PluginSignatureStatus = "internal" // core plugin, no signature
|
||||
pluginSignatureValid PluginSignatureStatus = "valid" // signed and accurate MANIFEST
|
||||
pluginSignatureInvalid PluginSignatureStatus = "invalid" // invalid signature
|
||||
pluginSignatureModified PluginSignatureStatus = "modified" // valid signature, but content mismatch
|
||||
pluginSignatureUnsigned PluginSignatureStatus = "unsigned" // no MANIFEST file
|
||||
)
|
||||
|
||||
type PluginSignatureType string
|
||||
|
||||
const (
|
||||
grafanaType PluginSignatureType = "grafana"
|
||||
privateType PluginSignatureType = "private"
|
||||
)
|
||||
|
||||
type PluginNotFoundError struct {
|
||||
PluginID string
|
||||
}
|
||||
@@ -60,25 +21,25 @@ func (e PluginNotFoundError) Error() string {
|
||||
return fmt.Sprintf("plugin with ID %q not found", e.PluginID)
|
||||
}
|
||||
|
||||
type duplicatePluginError struct {
|
||||
type DuplicatePluginError struct {
|
||||
Plugin *PluginBase
|
||||
ExistingPlugin *PluginBase
|
||||
}
|
||||
|
||||
func (e duplicatePluginError) Error() string {
|
||||
func (e DuplicatePluginError) Error() string {
|
||||
return fmt.Sprintf("plugin with ID %q already loaded from %q", e.Plugin.Id, e.ExistingPlugin.PluginDir)
|
||||
}
|
||||
|
||||
func (e duplicatePluginError) Is(err error) bool {
|
||||
func (e DuplicatePluginError) Is(err error) bool {
|
||||
// nolint:errorlint
|
||||
_, ok := err.(duplicatePluginError)
|
||||
_, ok := err.(DuplicatePluginError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// PluginLoader can load a plugin.
|
||||
type PluginLoader interface {
|
||||
// Load loads a plugin and registers it with the manager.
|
||||
Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) error
|
||||
// Load loads a plugin and returns it.
|
||||
Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) (interface{}, error)
|
||||
}
|
||||
|
||||
// PluginBase is the base plugin type.
|
||||
@@ -112,39 +73,6 @@ type PluginBase struct {
|
||||
Root *PluginBase
|
||||
}
|
||||
|
||||
func (pb *PluginBase) registerPlugin(base *PluginBase) error {
|
||||
if p, exists := Plugins[pb.Id]; exists {
|
||||
return duplicatePluginError{Plugin: pb, ExistingPlugin: p}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(base.PluginDir, setting.StaticRootPath) {
|
||||
plog.Info("Registering plugin", "id", pb.Id)
|
||||
}
|
||||
|
||||
if len(pb.Dependencies.Plugins) == 0 {
|
||||
pb.Dependencies.Plugins = []PluginDependencyItem{}
|
||||
}
|
||||
|
||||
if pb.Dependencies.GrafanaVersion == "" {
|
||||
pb.Dependencies.GrafanaVersion = "*"
|
||||
}
|
||||
|
||||
for _, include := range pb.Includes {
|
||||
if include.Role == "" {
|
||||
include.Role = models.ROLE_VIEWER
|
||||
}
|
||||
}
|
||||
|
||||
// Copy relevant fields from the base
|
||||
pb.PluginDir = base.PluginDir
|
||||
pb.Signature = base.Signature
|
||||
pb.SignatureType = base.SignatureType
|
||||
pb.SignatureOrg = base.SignatureOrg
|
||||
|
||||
Plugins[pb.Id] = pb
|
||||
return nil
|
||||
}
|
||||
|
||||
type PluginDependencies struct {
|
||||
GrafanaVersion string `json:"grafanaVersion"`
|
||||
Plugins []PluginDependencyItem `json:"plugins"`
|
||||
@@ -214,11 +142,3 @@ type EnabledPlugins struct {
|
||||
DataSources map[string]*DataSourcePlugin
|
||||
Apps []*AppPlugin
|
||||
}
|
||||
|
||||
func NewEnabledPlugins() EnabledPlugins {
|
||||
return EnabledPlugins{
|
||||
Panels: make([]*PanelPlugin, 0),
|
||||
DataSources: make(map[string]*DataSourcePlugin),
|
||||
Apps: make([]*AppPlugin, 0),
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,11 @@ type PanelPlugin struct {
|
||||
SkipDataQuery bool `json:"skipDataQuery"`
|
||||
}
|
||||
|
||||
func (p *PanelPlugin) Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) error {
|
||||
func (p *PanelPlugin) Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) (
|
||||
interface{}, error) {
|
||||
if err := decoder.Decode(p); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := p.registerPlugin(base); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Panels[p.Id] = p
|
||||
return nil
|
||||
return p, nil
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package plugindashboards
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register(®istry.Descriptor{
|
||||
Name: "PluginDashboardService",
|
||||
Instance: &Service{},
|
||||
})
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
DataService *tsdb.Service `inject:""`
|
||||
PluginManager *manager.PluginManager `inject:""`
|
||||
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (s *Service) Init() error {
|
||||
bus.AddEventListener(s.handlePluginStateChanged)
|
||||
s.logger = log.New("plugindashboards")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Run(ctx context.Context) error {
|
||||
s.updateAppDashboards()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) updateAppDashboards() {
|
||||
s.logger.Debug("Looking for app dashboard updates")
|
||||
|
||||
query := models.GetPluginSettingsQuery{OrgId: 0}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
s.logger.Error("Failed to get all plugin settings", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, pluginSetting := range query.Result {
|
||||
// ignore disabled plugins
|
||||
if !pluginSetting.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if pluginDef, exists := manager.Plugins[pluginSetting.PluginId]; exists {
|
||||
if pluginDef.Info.Version != pluginSetting.PluginVersion {
|
||||
s.syncPluginDashboards(pluginDef, pluginSetting.OrgId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) syncPluginDashboards(pluginDef *plugins.PluginBase, orgID int64) {
|
||||
s.logger.Info("Syncing plugin dashboards to DB", "pluginId", pluginDef.Id)
|
||||
|
||||
// Get plugin dashboards
|
||||
dashboards, err := s.PluginManager.GetPluginDashboards(orgID, pluginDef.Id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to load app dashboards", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Update dashboards with updated revisions
|
||||
for _, dash := range dashboards {
|
||||
// remove removed ones
|
||||
if dash.Removed {
|
||||
s.logger.Info("Deleting plugin dashboard", "pluginId", pluginDef.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)
|
||||
return
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// update updated ones
|
||||
if dash.ImportedRevision != dash.Revision {
|
||||
if err := s.autoUpdateAppDashboard(dash, orgID); err != nil {
|
||||
s.logger.Error("Failed to auto update app dashboard", "pluginId", pluginDef.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}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
s.logger.Error("Failed to read plugin setting by ID", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
appSetting := query.Result
|
||||
cmd := models.UpdatePluginSettingVersionCmd{
|
||||
OrgId: appSetting.OrgId,
|
||||
PluginId: appSetting.PluginId,
|
||||
PluginVersion: pluginDef.Info.Version,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
s.logger.Error("Failed to update plugin setting version", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) handlePluginStateChanged(event *models.PluginStateChangedEvent) error {
|
||||
s.logger.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled)
|
||||
|
||||
if event.Enabled {
|
||||
s.syncPluginDashboards(manager.Plugins[event.PluginId], event.OrgId)
|
||||
} else {
|
||||
query := models.GetDashboardsByPluginIdQuery{PluginId: event.PluginId, OrgId: event.OrgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, dash := range query.Result {
|
||||
s.logger.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
|
||||
deleteCmd := models.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id}
|
||||
if err := bus.Dispatch(&deleteCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) autoUpdateAppDashboard(pluginDashInfo *plugins.PluginDashboardInfoDTO, orgID int64) error {
|
||||
dash, err := s.PluginManager.LoadPluginDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logger.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev",
|
||||
pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision)
|
||||
user := &models.SignedInUser{UserId: 0, OrgRole: models.ROLE_ADMIN}
|
||||
_, err = s.PluginManager.ImportDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path, orgID, 0, dash.Data, true,
|
||||
nil, user, s.DataService)
|
||||
return err
|
||||
}
|
||||
+1
-540
@@ -1,541 +1,2 @@
|
||||
// Package plugins contains plugin related logic.
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
var (
|
||||
DataSources map[string]*DataSourcePlugin
|
||||
Panels map[string]*PanelPlugin
|
||||
StaticRoutes []*PluginStaticRoute
|
||||
Apps map[string]*AppPlugin
|
||||
Plugins map[string]*PluginBase
|
||||
PluginTypes map[string]interface{}
|
||||
Renderer *RendererPlugin
|
||||
|
||||
plog log.Logger
|
||||
)
|
||||
|
||||
type unsignedPluginConditionFunc = func(plugin *PluginBase) bool
|
||||
|
||||
type PluginScanner struct {
|
||||
pluginPath string
|
||||
errors []error
|
||||
backendPluginManager backendplugin.Manager
|
||||
cfg *setting.Cfg
|
||||
requireSigned bool
|
||||
log log.Logger
|
||||
plugins map[string]*PluginBase
|
||||
allowUnsignedPluginsCondition unsignedPluginConditionFunc
|
||||
}
|
||||
|
||||
type PluginManager struct {
|
||||
BackendPluginManager backendplugin.Manager `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
log log.Logger
|
||||
scanningErrors []error
|
||||
|
||||
// AllowUnsignedPluginsCondition changes the policy for allowing unsigned plugins. Signature validation only runs when plugins are starting
|
||||
// and running plugins will not be terminated if they violate the new policy.
|
||||
AllowUnsignedPluginsCondition unsignedPluginConditionFunc
|
||||
GrafanaLatestVersion string
|
||||
GrafanaHasUpdate bool
|
||||
pluginScanningErrors map[string]PluginError
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.RegisterService(&PluginManager{})
|
||||
}
|
||||
|
||||
func (pm *PluginManager) Init() error {
|
||||
pm.log = log.New("plugins")
|
||||
plog = log.New("plugins")
|
||||
|
||||
DataSources = map[string]*DataSourcePlugin{}
|
||||
StaticRoutes = []*PluginStaticRoute{}
|
||||
Panels = map[string]*PanelPlugin{}
|
||||
Apps = map[string]*AppPlugin{}
|
||||
Plugins = map[string]*PluginBase{}
|
||||
PluginTypes = map[string]interface{}{
|
||||
"panel": PanelPlugin{},
|
||||
"datasource": DataSourcePlugin{},
|
||||
"app": AppPlugin{},
|
||||
"renderer": RendererPlugin{},
|
||||
}
|
||||
pm.pluginScanningErrors = map[string]PluginError{}
|
||||
|
||||
pm.log.Info("Starting plugin search")
|
||||
|
||||
plugDir := filepath.Join(pm.Cfg.StaticRootPath, "app/plugins")
|
||||
pm.log.Debug("Scanning core plugin directory", "dir", plugDir)
|
||||
if err := pm.scan(plugDir, false); err != nil {
|
||||
return errutil.Wrapf(err, "failed to scan core plugin directory '%s'", plugDir)
|
||||
}
|
||||
|
||||
plugDir = pm.Cfg.BundledPluginsPath
|
||||
pm.log.Debug("Scanning bundled plugins directory", "dir", plugDir)
|
||||
exists, err := fs.Exists(plugDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
if err := pm.scan(plugDir, false); err != nil {
|
||||
return errutil.Wrapf(err, "failed to scan bundled plugins directory '%s'", plugDir)
|
||||
}
|
||||
}
|
||||
|
||||
// check if plugins dir exists
|
||||
exists, err = fs.Exists(pm.Cfg.PluginsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
if err = os.MkdirAll(pm.Cfg.PluginsPath, os.ModePerm); err != nil {
|
||||
pm.log.Error("failed to create external plugins directory", "dir", pm.Cfg.PluginsPath, "error", err)
|
||||
} else {
|
||||
pm.log.Info("External plugins directory created", "directory", pm.Cfg.PluginsPath)
|
||||
}
|
||||
} else {
|
||||
pm.log.Debug("Scanning external plugins directory", "dir", pm.Cfg.PluginsPath)
|
||||
if err := pm.scan(pm.Cfg.PluginsPath, true); err != nil {
|
||||
return errutil.Wrapf(err, "failed to scan external plugins directory '%s'",
|
||||
pm.Cfg.PluginsPath)
|
||||
}
|
||||
}
|
||||
|
||||
if err := pm.scanPluginPaths(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, panel := range Panels {
|
||||
panel.initFrontendPlugin()
|
||||
}
|
||||
|
||||
for _, ds := range DataSources {
|
||||
ds.initFrontendPlugin()
|
||||
}
|
||||
|
||||
for _, app := range Apps {
|
||||
app.initApp()
|
||||
}
|
||||
|
||||
if Renderer != nil {
|
||||
Renderer.initFrontendPlugin()
|
||||
}
|
||||
|
||||
for _, p := range Plugins {
|
||||
if p.IsCorePlugin {
|
||||
p.Signature = pluginSignatureInternal
|
||||
} else {
|
||||
metrics.SetPluginBuildInformation(p.Id, p.Type, p.Info.Version)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *PluginManager) Run(ctx context.Context) error {
|
||||
pm.updateAppDashboards()
|
||||
pm.checkForUpdates()
|
||||
|
||||
ticker := time.NewTicker(time.Minute * 10)
|
||||
run := true
|
||||
|
||||
for run {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
pm.checkForUpdates()
|
||||
case <-ctx.Done():
|
||||
run = false
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// scanPluginPaths scans configured plugin paths.
|
||||
func (pm *PluginManager) scanPluginPaths() error {
|
||||
for pluginID, settings := range pm.Cfg.PluginSettings {
|
||||
path, exists := settings["path"]
|
||||
if !exists || path == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := pm.scan(path, true); err != nil {
|
||||
return errutil.Wrapf(err, "failed to scan directory configured for plugin '%s': '%s'", pluginID, path)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scan a directory for plugins.
|
||||
func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error {
|
||||
scanner := &PluginScanner{
|
||||
pluginPath: pluginDir,
|
||||
backendPluginManager: pm.BackendPluginManager,
|
||||
cfg: pm.Cfg,
|
||||
requireSigned: requireSigned,
|
||||
log: pm.log,
|
||||
plugins: map[string]*PluginBase{},
|
||||
allowUnsignedPluginsCondition: pm.AllowUnsignedPluginsCondition,
|
||||
}
|
||||
|
||||
// 1st pass: Scan plugins, also mapping plugins to their respective directories
|
||||
if err := util.Walk(pluginDir, true, true, scanner.walker); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
pm.log.Debug("Couldn't scan directory since it doesn't exist", "pluginDir", pluginDir, "err", err)
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
pm.log.Debug("Couldn't scan directory due to lack of permissions", "pluginDir", pluginDir, "err", err)
|
||||
return nil
|
||||
}
|
||||
if pluginDir != "data/plugins" {
|
||||
pm.log.Warn("Could not scan dir", "pluginDir", pluginDir, "err", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
pm.log.Debug("Initial plugin loading done")
|
||||
|
||||
// 2nd pass: Validate and register plugins
|
||||
for dpath, plugin := range scanner.plugins {
|
||||
// Try to find any root plugin
|
||||
ancestors := strings.Split(dpath, string(filepath.Separator))
|
||||
ancestors = ancestors[0 : len(ancestors)-1]
|
||||
aPath := ""
|
||||
if runtime.GOOS != "windows" && filepath.IsAbs(dpath) {
|
||||
aPath = "/"
|
||||
}
|
||||
for _, a := range ancestors {
|
||||
aPath = filepath.Join(aPath, a)
|
||||
if root, ok := scanner.plugins[aPath]; ok {
|
||||
plugin.Root = root
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pm.log.Debug("Found plugin", "id", plugin.Id, "signature", plugin.Signature, "hasRoot", plugin.Root != nil)
|
||||
signingError := scanner.validateSignature(plugin)
|
||||
if signingError != nil {
|
||||
pm.log.Debug("Failed to validate plugin signature. Will skip loading", "id", plugin.Id,
|
||||
"signature", plugin.Signature, "status", signingError.ErrorCode)
|
||||
pm.pluginScanningErrors[plugin.Id] = *signingError
|
||||
continue
|
||||
}
|
||||
|
||||
pm.log.Debug("Attempting to add plugin", "id", plugin.Id)
|
||||
|
||||
pluginGoType, exists := PluginTypes[plugin.Type]
|
||||
if !exists {
|
||||
return fmt.Errorf("unknown plugin type %q", plugin.Type)
|
||||
}
|
||||
|
||||
jsonFPath := filepath.Join(plugin.PluginDir, "plugin.json")
|
||||
|
||||
// External plugins need a module.js file for SystemJS to load
|
||||
if !strings.HasPrefix(jsonFPath, pm.Cfg.StaticRootPath) && !scanner.IsBackendOnlyPlugin(plugin.Type) {
|
||||
module := filepath.Join(plugin.PluginDir, "module.js")
|
||||
exists, err := fs.Exists(module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
scanner.log.Warn("Plugin missing module.js",
|
||||
"name", plugin.Name,
|
||||
"warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.",
|
||||
"path", module)
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `jsonFPath` is based
|
||||
// on plugin the folder structure on disk and not user input.
|
||||
reader, err := os.Open(jsonFPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := reader.Close(); err != nil {
|
||||
scanner.log.Warn("Failed to close JSON file", "path", jsonFPath, "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
jsonParser := json.NewDecoder(reader)
|
||||
|
||||
loader := reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader)
|
||||
|
||||
// Load the full plugin, and add it to manager
|
||||
if err := loader.Load(jsonParser, plugin, scanner.backendPluginManager); err != nil {
|
||||
if errors.Is(err, duplicatePluginError{}) {
|
||||
pm.log.Warn("Plugin is duplicate", "error", err)
|
||||
scanner.errors = append(scanner.errors, err)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
pm.log.Debug("Successfully added plugin", "id", plugin.Id)
|
||||
}
|
||||
|
||||
if len(scanner.errors) > 0 {
|
||||
pm.log.Warn("Some plugins failed to load", "errors", scanner.errors)
|
||||
pm.scanningErrors = scanner.errors
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDatasource returns a datasource based on passed pluginID if it exists
|
||||
//
|
||||
// This function fetches the datasource from the global variable DataSources in this package.
|
||||
// Rather then refactor all dependencies on the global variable we can use this as an transition.
|
||||
func (pm *PluginManager) GetDatasource(pluginID string) (*DataSourcePlugin, bool) {
|
||||
ds, exist := DataSources[pluginID]
|
||||
return ds, exist
|
||||
}
|
||||
|
||||
func (s *PluginScanner) walker(currentPath string, f os.FileInfo, err error) error {
|
||||
// We scan all the subfolders for plugin.json (with some exceptions) so that we also load embedded plugins, for
|
||||
// example https://github.com/raintank/worldping-app/tree/master/dist/grafana-worldmap-panel worldmap panel plugin
|
||||
// is embedded in worldping app.
|
||||
if err != nil {
|
||||
return fmt.Errorf("filepath.Walk reported an error for %q: %w", currentPath, err)
|
||||
}
|
||||
|
||||
if f.Name() == "node_modules" || f.Name() == "Chromium.app" {
|
||||
return util.ErrWalkSkipDir
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if f.Name() != "plugin.json" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.loadPlugin(currentPath); err != nil {
|
||||
s.log.Error("Failed to load plugin", "error", err, "pluginPath", filepath.Dir(currentPath))
|
||||
s.errors = append(s.errors, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PluginScanner) loadPlugin(pluginJSONFilePath string) error {
|
||||
s.log.Debug("Loading plugin", "path", pluginJSONFilePath)
|
||||
currentDir := filepath.Dir(pluginJSONFilePath)
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `currentPath` is based
|
||||
// on plugin the folder structure on disk and not user input.
|
||||
reader, err := os.Open(pluginJSONFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := reader.Close(); err != nil {
|
||||
s.log.Warn("Failed to close JSON file", "path", pluginJSONFilePath, "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
jsonParser := json.NewDecoder(reader)
|
||||
pluginCommon := PluginBase{}
|
||||
if err := jsonParser.Decode(&pluginCommon); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pluginCommon.Id == "" || pluginCommon.Type == "" {
|
||||
return errors.New("did not find type or id properties in plugin.json")
|
||||
}
|
||||
|
||||
pluginCommon.PluginDir = filepath.Dir(pluginJSONFilePath)
|
||||
pluginCommon.Files, err = collectPluginFilesWithin(pluginCommon.PluginDir)
|
||||
if err != nil {
|
||||
s.log.Warn("Could not collect plugin file information in directory", "pluginID", pluginCommon.Id, "dir", pluginCommon.PluginDir)
|
||||
return err
|
||||
}
|
||||
|
||||
signatureState, err := getPluginSignatureState(s.log, &pluginCommon)
|
||||
if err != nil {
|
||||
s.log.Warn("Could not get plugin signature state", "pluginID", pluginCommon.Id, "err", err)
|
||||
return err
|
||||
}
|
||||
pluginCommon.Signature = signatureState.Status
|
||||
pluginCommon.SignatureType = signatureState.Type
|
||||
pluginCommon.SignatureOrg = signatureState.SigningOrg
|
||||
|
||||
s.plugins[currentDir] = &pluginCommon
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*PluginScanner) IsBackendOnlyPlugin(pluginType string) bool {
|
||||
return pluginType == "renderer"
|
||||
}
|
||||
|
||||
// validateSignature validates a plugin's signature.
|
||||
func (s *PluginScanner) validateSignature(plugin *PluginBase) *PluginError {
|
||||
if plugin.Signature == pluginSignatureValid {
|
||||
s.log.Debug("Plugin has valid signature", "id", plugin.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
if plugin.Root != nil {
|
||||
// If a descendant plugin with invalid signature, set signature to that of root
|
||||
if plugin.IsCorePlugin || plugin.Signature == pluginSignatureInternal {
|
||||
s.log.Debug("Not setting descendant plugin's signature to that of root since it's core or internal",
|
||||
"plugin", plugin.Id, "signature", plugin.Signature, "isCore", plugin.IsCorePlugin)
|
||||
} else {
|
||||
s.log.Debug("Setting descendant plugin's signature to that of root", "plugin", plugin.Id,
|
||||
"root", plugin.Root.Id, "signature", plugin.Signature, "rootSignature", plugin.Root.Signature)
|
||||
plugin.Signature = plugin.Root.Signature
|
||||
if plugin.Signature == pluginSignatureValid {
|
||||
s.log.Debug("Plugin has valid signature (inherited from root)", "id", plugin.Id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.log.Debug("Non-valid plugin Signature", "pluginID", plugin.Id, "pluginDir", plugin.PluginDir,
|
||||
"state", plugin.Signature)
|
||||
}
|
||||
|
||||
// For the time being, we choose to only require back-end plugins to be signed
|
||||
// NOTE: the state is calculated again when setting metadata on the object
|
||||
if !plugin.Backend || !s.requireSigned {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch plugin.Signature {
|
||||
case pluginSignatureUnsigned:
|
||||
if allowed := s.allowUnsigned(plugin); !allowed {
|
||||
s.log.Debug("Plugin is unsigned", "id", plugin.Id)
|
||||
s.errors = append(s.errors, fmt.Errorf("plugin %q is unsigned", plugin.Id))
|
||||
return &PluginError{
|
||||
ErrorCode: signatureMissing,
|
||||
}
|
||||
}
|
||||
s.log.Warn("Running an unsigned backend plugin", "pluginID", plugin.Id, "pluginDir",
|
||||
plugin.PluginDir)
|
||||
return nil
|
||||
case pluginSignatureInvalid:
|
||||
s.log.Debug("Plugin %q has an invalid signature", plugin.Id)
|
||||
s.errors = append(s.errors, fmt.Errorf("plugin %q has an invalid signature", plugin.Id))
|
||||
return &PluginError{
|
||||
ErrorCode: signatureInvalid,
|
||||
}
|
||||
case pluginSignatureModified:
|
||||
s.log.Debug("Plugin %q has a modified signature", plugin.Id)
|
||||
s.errors = append(s.errors, fmt.Errorf("plugin %q's signature has been modified", plugin.Id))
|
||||
return &PluginError{
|
||||
ErrorCode: signatureModified,
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("Plugin %q has unrecognized plugin signature state %q", plugin.Id, plugin.Signature))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PluginScanner) allowUnsigned(plugin *PluginBase) bool {
|
||||
if s.allowUnsignedPluginsCondition != nil {
|
||||
return s.allowUnsignedPluginsCondition(plugin)
|
||||
}
|
||||
|
||||
if s.cfg.Env == setting.Dev {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, plug := range s.cfg.PluginsAllowUnsigned {
|
||||
if plug == plugin.Id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ScanningErrors returns plugin scanning errors encountered.
|
||||
func (pm *PluginManager) ScanningErrors() []PluginError {
|
||||
scanningErrs := make([]PluginError, 0)
|
||||
for id, e := range pm.pluginScanningErrors {
|
||||
scanningErrs = append(scanningErrs, PluginError{
|
||||
ErrorCode: e.ErrorCode,
|
||||
PluginID: id,
|
||||
})
|
||||
}
|
||||
return scanningErrs
|
||||
}
|
||||
|
||||
func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
|
||||
plug, exists := Plugins[pluginId]
|
||||
if !exists {
|
||||
return nil, PluginNotFoundError{pluginId}
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `plug.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)))
|
||||
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)))
|
||||
}
|
||||
|
||||
exists, err = fs.Exists(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return make([]byte, 0), nil
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based
|
||||
// on plugin the folder structure on disk and not user input.
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// gets plugin filenames that require verification for plugin signing
|
||||
func collectPluginFilesWithin(rootDir string) ([]string, error) {
|
||||
var files []string
|
||||
|
||||
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() && info.Name() != "MANIFEST.txt" {
|
||||
file, err := filepath.Rel(rootDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files = append(files, filepath.ToSlash(file))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return files, err
|
||||
}
|
||||
@@ -22,29 +22,25 @@ type RendererPlugin struct {
|
||||
backendPluginManager backendplugin.Manager
|
||||
}
|
||||
|
||||
func (r *RendererPlugin) Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) error {
|
||||
func (r *RendererPlugin) Load(decoder *json.Decoder, base *PluginBase,
|
||||
backendPluginManager backendplugin.Manager) (interface{}, error) {
|
||||
if err := decoder.Decode(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.registerPlugin(base); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.backendPluginManager = backendPluginManager
|
||||
|
||||
cmd := ComposePluginStartCommand("plugin_start")
|
||||
fullpath := filepath.Join(r.PluginDir, cmd)
|
||||
fullpath := filepath.Join(base.PluginDir, cmd)
|
||||
factory := grpcplugin.NewRendererPlugin(r.Id, fullpath, grpcplugin.PluginStartFuncs{
|
||||
OnLegacyStart: r.onLegacyPluginStart,
|
||||
OnStart: r.onPluginStart,
|
||||
})
|
||||
if err := backendPluginManager.Register(r.Id, factory); err != nil {
|
||||
return errutil.Wrapf(err, "Failed to register backend plugin")
|
||||
return nil, errutil.Wrapf(err, "failed to register backend plugin")
|
||||
}
|
||||
|
||||
Renderer = r
|
||||
return nil
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *RendererPlugin) Start(ctx context.Context) error {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package plugins
|
||||
|
||||
type PluginSignatureStatus string
|
||||
|
||||
func (pss PluginSignatureStatus) IsValid() bool {
|
||||
return pss == PluginSignatureValid
|
||||
}
|
||||
|
||||
func (pss PluginSignatureStatus) IsInternal() bool {
|
||||
return pss == PluginSignatureInternal
|
||||
}
|
||||
|
||||
const (
|
||||
PluginSignatureInternal PluginSignatureStatus = "internal" // core plugin, no signature
|
||||
PluginSignatureValid PluginSignatureStatus = "valid" // signed and accurate MANIFEST
|
||||
PluginSignatureInvalid PluginSignatureStatus = "invalid" // invalid signature
|
||||
PluginSignatureModified PluginSignatureStatus = "modified" // valid signature, but content mismatch
|
||||
PluginSignatureUnsigned PluginSignatureStatus = "unsigned" // no MANIFEST file
|
||||
)
|
||||
|
||||
type PluginState string
|
||||
|
||||
const (
|
||||
PluginStateAlpha PluginState = "alpha"
|
||||
)
|
||||
|
||||
type PluginSignatureType string
|
||||
|
||||
const (
|
||||
GrafanaType PluginSignatureType = "grafana"
|
||||
PrivateType PluginSignatureType = "private"
|
||||
)
|
||||
|
||||
type PluginSignatureState struct {
|
||||
Status PluginSignatureStatus
|
||||
Type PluginSignatureType
|
||||
SigningOrg string
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/timberio/go-datemath"
|
||||
)
|
||||
|
||||
// DataSubQuery represents a data sub-query.
|
||||
type DataSubQuery struct {
|
||||
RefID string `json:"refId"`
|
||||
Model *simplejson.Json `json:"model,omitempty"`
|
||||
DataSource *models.DataSource `json:"datasource"`
|
||||
MaxDataPoints int64 `json:"maxDataPoints"`
|
||||
IntervalMS int64 `json:"intervalMs"`
|
||||
QueryType string `json:"queryType"`
|
||||
}
|
||||
|
||||
// DataQuery contains all information about a data query request.
|
||||
type DataQuery struct {
|
||||
TimeRange *DataTimeRange
|
||||
Queries []DataSubQuery
|
||||
Headers map[string]string
|
||||
Debug bool
|
||||
User *models.SignedInUser
|
||||
}
|
||||
|
||||
type DataTimeRange struct {
|
||||
From string
|
||||
To string
|
||||
Now time.Time
|
||||
}
|
||||
|
||||
type DataTable struct {
|
||||
Columns []DataTableColumn `json:"columns"`
|
||||
Rows []DataRowValues `json:"rows"`
|
||||
}
|
||||
|
||||
type DataTableColumn struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type DataTimePoint [2]null.Float
|
||||
type DataTimeSeriesPoints []DataTimePoint
|
||||
type DataTimeSeriesSlice []DataTimeSeries
|
||||
type DataRowValues []interface{}
|
||||
|
||||
type DataQueryResult struct {
|
||||
Error error `json:"-"`
|
||||
ErrorString string `json:"error,omitempty"`
|
||||
RefID string `json:"refId"`
|
||||
Meta *simplejson.Json `json:"meta,omitempty"`
|
||||
Series DataTimeSeriesSlice `json:"series"`
|
||||
Tables []DataTable `json:"tables"`
|
||||
Dataframes DataFrames `json:"dataframes"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON deserializes a DataQueryResult from JSON.
|
||||
//
|
||||
// Deserialization support is required by tests.
|
||||
func (r *DataQueryResult) UnmarshalJSON(b []byte) error {
|
||||
m := map[string]interface{}{}
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
refID, ok := m["refId"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("can't decode field refId - not a string")
|
||||
}
|
||||
var meta *simplejson.Json
|
||||
if m["meta"] != nil {
|
||||
mm, ok := m["meta"].(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("can't decode field meta - not a JSON object")
|
||||
}
|
||||
meta = simplejson.NewFromAny(mm)
|
||||
}
|
||||
var series DataTimeSeriesSlice
|
||||
/* TODO
|
||||
if m["series"] != nil {
|
||||
}
|
||||
*/
|
||||
var tables []DataTable
|
||||
if m["tables"] != nil {
|
||||
ts, ok := m["tables"].([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("can't decode field tables - not an array of Tables")
|
||||
}
|
||||
for _, ti := range ts {
|
||||
tm, ok := ti.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("can't decode field tables - not an array of Tables")
|
||||
}
|
||||
var columns []DataTableColumn
|
||||
cs, ok := tm["columns"].([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("can't decode field tables - not an array of Tables")
|
||||
}
|
||||
for _, ci := range cs {
|
||||
cm, ok := ci.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("can't decode field tables - not an array of Tables")
|
||||
}
|
||||
val, ok := cm["text"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("can't decode field tables - not an array of Tables")
|
||||
}
|
||||
|
||||
columns = append(columns, DataTableColumn{Text: val})
|
||||
}
|
||||
|
||||
rs, ok := tm["rows"].([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("can't decode field tables - not an array of Tables")
|
||||
}
|
||||
var rows []DataRowValues
|
||||
for _, ri := range rs {
|
||||
vals, ok := ri.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("can't decode field tables - not an array of Tables")
|
||||
}
|
||||
rows = append(rows, vals)
|
||||
}
|
||||
|
||||
tables = append(tables, DataTable{
|
||||
Columns: columns,
|
||||
Rows: rows,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var dfs *dataFrames
|
||||
if m["dataframes"] != nil {
|
||||
raw, ok := m["dataframes"].([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("can't decode field dataframes - not an array of byte arrays")
|
||||
}
|
||||
|
||||
var encoded [][]byte
|
||||
for _, ra := range raw {
|
||||
encS, ok := ra.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("can't decode field dataframes - not an array of byte arrays")
|
||||
}
|
||||
enc, err := base64.StdEncoding.DecodeString(encS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't decode field dataframes - not an array of arrow frames")
|
||||
}
|
||||
encoded = append(encoded, enc)
|
||||
}
|
||||
decoded, err := data.UnmarshalArrowFrames(encoded)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dfs = &dataFrames{
|
||||
decoded: decoded,
|
||||
encoded: encoded,
|
||||
}
|
||||
}
|
||||
|
||||
r.RefID = refID
|
||||
r.Meta = meta
|
||||
r.Series = series
|
||||
r.Tables = tables
|
||||
if dfs != nil {
|
||||
r.Dataframes = dfs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DataTimeSeries struct {
|
||||
Name string `json:"name"`
|
||||
Points DataTimeSeriesPoints `json:"points"`
|
||||
Tags map[string]string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
type DataResponse struct {
|
||||
Results map[string]DataQueryResult `json:"results"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type DataPlugin interface {
|
||||
DataQuery(ctx context.Context, ds *models.DataSource, query DataQuery) (DataResponse, error)
|
||||
}
|
||||
|
||||
func NewDataTimeRange(from, to string) DataTimeRange {
|
||||
return DataTimeRange{
|
||||
From: from,
|
||||
To: to,
|
||||
Now: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *DataTimeRange) GetFromAsMsEpoch() int64 {
|
||||
return tr.MustGetFrom().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
func (tr *DataTimeRange) GetFromAsSecondsEpoch() int64 {
|
||||
return tr.GetFromAsMsEpoch() / 1000
|
||||
}
|
||||
|
||||
func (tr *DataTimeRange) GetFromAsTimeUTC() time.Time {
|
||||
return tr.MustGetFrom().UTC()
|
||||
}
|
||||
|
||||
func (tr *DataTimeRange) GetToAsMsEpoch() int64 {
|
||||
return tr.MustGetTo().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
func (tr *DataTimeRange) GetToAsSecondsEpoch() int64 {
|
||||
return tr.GetToAsMsEpoch() / 1000
|
||||
}
|
||||
|
||||
func (tr *DataTimeRange) GetToAsTimeUTC() time.Time {
|
||||
return tr.MustGetTo().UTC()
|
||||
}
|
||||
|
||||
func (tr *DataTimeRange) MustGetFrom() time.Time {
|
||||
res, err := tr.ParseFrom()
|
||||
if err != nil {
|
||||
return time.Unix(0, 0)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (tr *DataTimeRange) MustGetTo() time.Time {
|
||||
res, err := tr.ParseTo()
|
||||
if err != nil {
|
||||
return time.Unix(0, 0)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (tr DataTimeRange) ParseFrom() (time.Time, error) {
|
||||
return parseTimeRange(tr.From, tr.Now, false, nil)
|
||||
}
|
||||
|
||||
func (tr DataTimeRange) ParseTo() (time.Time, error) {
|
||||
return parseTimeRange(tr.To, tr.Now, true, nil)
|
||||
}
|
||||
|
||||
func (tr DataTimeRange) ParseFromWithLocation(location *time.Location) (time.Time, error) {
|
||||
return parseTimeRange(tr.From, tr.Now, false, location)
|
||||
}
|
||||
|
||||
func (tr DataTimeRange) ParseToWithLocation(location *time.Location) (time.Time, error) {
|
||||
return parseTimeRange(tr.To, tr.Now, true, location)
|
||||
}
|
||||
|
||||
func parseTimeRange(s string, now time.Time, withRoundUp bool, location *time.Location) (time.Time, error) {
|
||||
if val, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||
seconds := val / 1000
|
||||
nano := (val - seconds*1000) * 1000000
|
||||
return time.Unix(seconds, nano), nil
|
||||
}
|
||||
|
||||
diff, err := time.ParseDuration("-" + s)
|
||||
if err != nil {
|
||||
options := []func(*datemath.Options){
|
||||
datemath.WithNow(now),
|
||||
datemath.WithRoundUp(withRoundUp),
|
||||
}
|
||||
if location != nil {
|
||||
options = append(options, datemath.WithLocation(location))
|
||||
}
|
||||
|
||||
return datemath.ParseAndEvaluate(s, options...)
|
||||
}
|
||||
|
||||
return now.Add(diff), nil
|
||||
}
|
||||
|
||||
// SeriesToFrame converts a DataTimeSeries to an SDK frame.
|
||||
func SeriesToFrame(series DataTimeSeries) (*data.Frame, error) {
|
||||
timeVec := make([]*time.Time, len(series.Points))
|
||||
floatVec := make([]*float64, len(series.Points))
|
||||
for idx, point := range series.Points {
|
||||
timeVec[idx], floatVec[idx] = convertDataTimePoint(point)
|
||||
}
|
||||
frame := data.NewFrame(series.Name,
|
||||
data.NewField("time", nil, timeVec),
|
||||
data.NewField("value", data.Labels(series.Tags), floatVec),
|
||||
)
|
||||
|
||||
return frame, nil
|
||||
}
|
||||
|
||||
// convertDataTimePoint converts a DataTimePoint into two values appropriate
|
||||
// for Series values.
|
||||
func convertDataTimePoint(point DataTimePoint) (t *time.Time, f *float64) {
|
||||
timeIdx, valueIdx := 1, 0
|
||||
if point[timeIdx].Valid { // Assuming valid is null?
|
||||
tI := int64(point[timeIdx].Float64)
|
||||
uT := time.Unix(tI/int64(1e+3), (tI%int64(1e+3))*int64(1e+6)) // time.Time from millisecond unix ts
|
||||
t = &uT
|
||||
}
|
||||
if point[valueIdx].Valid {
|
||||
f = &point[valueIdx].Float64
|
||||
}
|
||||
return
|
||||
}
|
||||
Reference in new issue
Block a user