The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/plugins/plugins.go

497 lines
12 KiB

package plugins
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"path"
"runtime"
"strings"
"sync"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/plugins/auth"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/plugindef"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/util"
)
var (
ErrFileNotExist = errors.New("file does not exist")
ErrPluginFileRead = errors.New("file could not be read")
ErrUninstallInvalidPluginDir = errors.New("cannot recognize as plugin folder")
ErrInvalidPluginJSON = errors.New("did not find valid type or id properties in plugin.json")
ErrUnsupportedAlias = errors.New("can not set alias in plugin.json")
)
type Plugin struct {
JSONData
FS FS
Class Class
// App fields
IncludedInAppID string
DefaultNavURL string
Pinned bool
// Signature fields
Signature SignatureStatus
SignatureType SignatureType
SignatureOrg string
Parent *Plugin
Children []*Plugin
SignatureError *SignatureError
// SystemJS fields
Module string
BaseURL string
Angular AngularMeta
ExternalService *auth.ExternalService
Renderer pluginextensionv2.RendererPlugin
SecretsManager secretsmanagerplugin.SecretsManagerPlugin
client backendplugin.Plugin
log log.Logger
SkipHostEnvVars bool
mu sync.Mutex
}
type AngularMeta struct {
Detected bool `json:"detected"`
HideDeprecation bool `json:"hideDeprecation"`
}
// JSONData represents the plugin's plugin.json
type JSONData struct {
// Common settings
ID string `json:"id"`
Type Type `json:"type"`
Name string `json:"name"`
AliasIDs []string `json:"aliasIDs,omitempty"`
Info Info `json:"info"`
Dependencies Dependencies `json:"dependencies"`
Includes []*Includes `json:"includes"`
State ReleaseState `json:"state,omitempty"`
Category string `json:"category"`
HideFromList bool `json:"hideFromList,omitempty"`
Preload bool `json:"preload"`
Backend bool `json:"backend"`
Routes []*Route `json:"routes"`
// AccessControl settings
Roles []RoleRegistration `json:"roles,omitempty"`
// Panel settings
SkipDataQuery bool `json:"skipDataQuery"`
// App settings
AutoEnabled bool `json:"autoEnabled"`
// Datasource settings
Annotations bool `json:"annotations"`
Metrics bool `json:"metrics"`
Alerting bool `json:"alerting"`
Explore bool `json:"explore"`
Table bool `json:"tables"`
Logs bool `json:"logs"`
Tracing bool `json:"tracing"`
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
BuiltIn bool `json:"builtIn,omitempty"`
Mixed bool `json:"mixed,omitempty"`
Streaming bool `json:"streaming"`
SDK bool `json:"sdk,omitempty"`
// Backend (Datasource + Renderer + SecretsManager)
Executable string `json:"executable,omitempty"`
// App Service Auth Registration
IAM *plugindef.IAM `json:"iam,omitempty"`
}
func ReadPluginJSON(reader io.Reader) (JSONData, error) {
plugin := JSONData{}
if err := json.NewDecoder(reader).Decode(&plugin); err != nil {
return JSONData{}, err
}
if err := validatePluginJSON(plugin); err != nil {
return JSONData{}, err
}
// Hardcoded changes
switch plugin.ID {
case "grafana-piechart-panel":
plugin.Name = "Pie Chart (old)"
case "grafana-pyroscope-datasource":
fallthrough
case "grafana-testdata-datasource":
fallthrough
case "grafana-postgresql-datasource":
fallthrough
case "annolist":
fallthrough
case "debug":
if len(plugin.AliasIDs) == 0 {
return plugin, fmt.Errorf("expected alias to be set")
}
default: // TODO: when gcom validates the alias, this condition can be removed
if len(plugin.AliasIDs) > 0 {
return plugin, ErrUnsupportedAlias
}
}
if len(plugin.Dependencies.Plugins) == 0 {
plugin.Dependencies.Plugins = []Dependency{}
}
if plugin.Dependencies.GrafanaVersion == "" {
plugin.Dependencies.GrafanaVersion = "*"
}
for _, include := range plugin.Includes {
if include.Role == "" {
include.Role = org.RoleViewer
}
}
return plugin, nil
}
func validatePluginJSON(data JSONData) error {
if data.ID == "" || !data.Type.IsValid() {
return ErrInvalidPluginJSON
}
return nil
}
func (d JSONData) DashboardIncludes() []*Includes {
result := []*Includes{}
for _, include := range d.Includes {
if include.Type == TypeDashboard {
result = append(result, include)
}
}
return result
}
// Route describes a plugin route that is defined in
// the plugin.json file for a plugin.
type Route struct {
Path string `json:"path"`
Method string `json:"method"`
ReqRole org.RoleType `json:"reqRole"`
ReqAction string `json:"reqAction"`
URL string `json:"url"`
URLParams []URLParam `json:"urlParams"`
Headers []Header `json:"headers"`
AuthType string `json:"authType"`
TokenAuth *JWTTokenAuth `json:"tokenAuth"`
JwtTokenAuth *JWTTokenAuth `json:"jwtTokenAuth"`
Body json.RawMessage `json:"body"`
}
func (r *Route) RequiresRBACAction() bool {
return r.ReqAction != ""
}
// Header describes an HTTP header that is forwarded with
// the proxied request for a plugin route
type Header struct {
Name string `json:"name"`
Content string `json:"content"`
}
// URLParam describes query string parameters for
// a url in a plugin route
type URLParam struct {
Name string `json:"name"`
Content string `json:"content"`
}
// JWTTokenAuth struct is both for normal Token Auth and JWT Token Auth with
// an uploaded JWT file.
type JWTTokenAuth struct {
Url string `json:"url"`
Scopes []string `json:"scopes"`
Params map[string]string `json:"params"`
}
func (p *Plugin) PluginID() string {
return p.ID
}
func (p *Plugin) Logger() log.Logger {
return p.log
}
func (p *Plugin) SetLogger(l log.Logger) {
p.log = l
}
func (p *Plugin) Start(ctx context.Context) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.client == nil {
return fmt.Errorf("could not start plugin %s as no plugin client exists", p.ID)
}
return p.client.Start(ctx)
}
func (p *Plugin) Stop(ctx context.Context) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.client == nil {
return nil
}
return p.client.Stop(ctx)
}
func (p *Plugin) IsManaged() bool {
if p.client != nil {
return p.client.IsManaged()
}
return false
}
func (p *Plugin) Decommission() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.client != nil {
return p.client.Decommission()
}
return nil
}
func (p *Plugin) IsDecommissioned() bool {
if p.client != nil {
return p.client.IsDecommissioned()
}
return false
}
func (p *Plugin) Exited() bool {
if p.client != nil {
return p.client.Exited()
}
return false
}
func (p *Plugin) Target() backendplugin.Target {
if !p.Backend {
return backendplugin.TargetNone
}
if p.client == nil {
return backendplugin.TargetUnknown
}
return p.client.Target()
}
func (p *Plugin) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
pluginClient, ok := p.Client()
if !ok {
return nil, ErrPluginUnavailable
}
return pluginClient.QueryData(ctx, req)
}
func (p *Plugin) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
pluginClient, ok := p.Client()
if !ok {
return ErrPluginUnavailable
}
return pluginClient.CallResource(ctx, req, sender)
}
func (p *Plugin) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
pluginClient, ok := p.Client()
if !ok {
return nil, ErrPluginUnavailable
}
return pluginClient.CheckHealth(ctx, req)
}
func (p *Plugin) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) {
pluginClient, ok := p.Client()
if !ok {
return nil, ErrPluginUnavailable
}
return pluginClient.CollectMetrics(ctx, req)
}
func (p *Plugin) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
pluginClient, ok := p.Client()
if !ok {
return nil, ErrPluginUnavailable
}
return pluginClient.SubscribeStream(ctx, req)
}
func (p *Plugin) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
pluginClient, ok := p.Client()
if !ok {
return nil, ErrPluginUnavailable
}
return pluginClient.PublishStream(ctx, req)
}
func (p *Plugin) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
pluginClient, ok := p.Client()
if !ok {
return ErrPluginUnavailable
}
return pluginClient.RunStream(ctx, req, sender)
}
func (p *Plugin) File(name string) (fs.File, error) {
cleanPath, err := util.CleanRelativePath(name)
if err != nil {
// CleanRelativePath should clean and make the path relative so this is not expected to fail
return nil, err
}
if p.FS == nil {
return nil, ErrFileNotExist
}
f, err := p.FS.Open(cleanPath)
if err != nil {
return nil, err
}
return f, nil
}
func (p *Plugin) RegisterClient(c backendplugin.Plugin) {
p.client = c
}
func (p *Plugin) Client() (PluginClient, bool) {
if p.client != nil {
return p.client, true
}
return nil, false
}
func (p *Plugin) ExecutablePath() string {
if p.IsRenderer() {
return p.executablePath("plugin_start")
}
if p.IsSecretsManager() {
return p.executablePath("secrets_plugin_start")
}
return p.executablePath(p.Executable)
}
func (p *Plugin) executablePath(f string) string {
os := strings.ToLower(runtime.GOOS)
arch := runtime.GOARCH
extension := ""
if os == "windows" {
extension = ".exe"
}
return path.Join(p.FS.Base(), fmt.Sprintf("%s_%s_%s%s", f, os, strings.ToLower(arch), extension))
}
type PluginClient interface {
backend.QueryDataHandler
backend.CollectMetricsHandler
backend.CheckHealthHandler
backend.CallResourceHandler
backend.StreamHandler
}
func (p *Plugin) StaticRoute() *StaticRoute {
if p.IsCorePlugin() {
return nil
}
if p.FS == nil {
return nil
}
return &StaticRoute{Directory: p.FS.Base(), PluginID: p.ID}
}
func (p *Plugin) IsRenderer() bool {
return p.Type == TypeRenderer
}
func (p *Plugin) IsSecretsManager() bool {
return p.Type == TypeSecretsManager
}
func (p *Plugin) IsApp() bool {
return p.Type == TypeApp
}
func (p *Plugin) IsCorePlugin() bool {
return p.Class == ClassCore
}
func (p *Plugin) IsBundledPlugin() bool {
return p.Class == ClassBundled
}
func (p *Plugin) IsExternalPlugin() bool {
return !p.IsCorePlugin() && !p.IsBundledPlugin()
}
type Class string
const (
ClassCore Class = "core"
ClassBundled Class = "bundled"
ClassExternal Class = "external"
)
func (c Class) String() string {
return string(c)
}
var PluginTypes = []Type{
TypeDataSource,
TypePanel,
TypeApp,
TypeRenderer,
TypeSecretsManager,
}
type Type string
const (
TypeDataSource Type = "datasource"
TypePanel Type = "panel"
TypeApp Type = "app"
TypeRenderer Type = "renderer"
TypeSecretsManager Type = "secretsmanager"
)
func (pt Type) IsValid() bool {
switch pt {
case TypeDataSource, TypePanel, TypeApp, TypeRenderer, TypeSecretsManager:
return true
}
return false
}