Plugins: Refactor Plugin Management (#40477)
* add core plugin flow * add instrumentation * move func * remove cruft * support external backend plugins * refactor + clean up * remove comments * refactor loader * simplify core plugin path arg * cleanup loggers * move signature validator to plugins package * fix sig packaging * cleanup plugin model * remove unnecessary plugin field * add start+stop for pm * fix failures * add decommissioned state * export fields just to get things flowing * fix comments * set static routes * make image loading idempotent * merge with backend plugin manager * re-use funcs * reorder imports + remove unnecessary interface * add some TODOs + remove unused func * remove unused instrumentation func * simplify client usage * remove import alias * re-use backendplugin.Plugin interface * re order funcs * improve var name * fix log statements * refactor data model * add logic for dupe check during loading * cleanup state setting * refactor loader * cleanup manager interface * add rendering flow * refactor loading + init * add renderer support * fix renderer plugin * reformat imports * track errors * fix plugin signature inheritance * name param in interface * update func comment * fix func arg name * introduce class concept * remove func * fix external plugin check * apply changes from pm-experiment * fix core plugins * fix imports * rename interface * comment API interface * add support for testdata plugin * enable alerting + use correct core plugin contracts * slim manager API * fix param name * fix filter * support static routes * fix rendering * tidy rendering * get tests compiling * fix install+uninstall * start finder test * add finder test coverage * start loader tests * add test for core plugins * load core + bundled test * add test for nested plugin loading * add test files * clean interface + fix registering some core plugins * refactoring * reformat and create sub packages * simplify core plugin init * fix ctx cancel scenario * migrate initializer * remove Init() funcs * add test starter * new logger * flesh out initializer tests * refactoring * remove unused svc * refactor rendering flow * fixup loader tests * add enabled helper func * fix logger name * fix data fetchers * fix case where plugin dir doesn't exist * improve coverage + move dupe checking to loader * remove noisy debug logs * register core plugins automagically * add support for renderer in catalog * make private func + fix req validation * use interface * re-add check for renderer in catalog * tidy up from moving to auto reg core plugins * core plugin registrar * guards * copy over core plugins for test infra * all tests green * renames * propagate new interfaces * kill old manager * get compiling * tidy up * update naming * refactor manager test + cleanup * add more cases to finder test * migrate validator to field * more coverage * refactor dupe checking * add test for plugin class * add coverage for initializer * split out rendering * move * fixup tests * fix uss test * fix frontend settings * fix grafanads test * add check when checking sig errors * fix enabled map * fixup * allow manual setup of CM * rename to cloud-monitoring * remove TODO * add installer interface for testing * loader interface returns * tests passing * refactor + add more coverage * support 'stackdriver' * fix frontend settings loading * improve naming based on package name * small tidy * refactor test * fix renderer start * make cloud-monitoring plugin ID clearer * add plugin update test * add integration tests * don't break all if sig can't be calculated * add root URL check test * add more signature verification tests * update DTO name * update enabled plugins comment * update comments * fix linter * revert fe naming change * fix errors endpoint * reset error code field name * re-order test to help verify * assert -> require * pm check * add missing entry + re-order * re-check * dump icon log * verify manager contents first * reformat * apply PR feedback * apply style changes * fix one vs all loading err * improve log output * only start when no signature error * move log * rework plugin update check * fix test * fix multi loading from cfg.PluginSettings * improve log output #2 * add error abstraction to capture errors without registering a plugin * add debug log * add unsigned warning * e2e test attempt * fix logger * set home path * prevent panic * alternate * ugh.. fix home path * return renderer even if not started * make renderer plugin managed * add fallback renderer icon, update renderer badge + prevent changes when renderer is installed * fix icon loading * rollback renderer changes * use correct field * remove unneccessary block * remove newline * remove unused func * fix bundled plugins base + module fields * remove unused field since refactor * add authorizer abstraction * loader only returns plugins expected to run * fix multi log output
This commit is contained in:
@@ -1,125 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"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/setting"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
type AppPlugin struct {
|
||||
FrontendPluginBase
|
||||
Routes []*AppPluginRoute `json:"routes"`
|
||||
AutoEnabled bool `json:"autoEnabled"`
|
||||
|
||||
FoundChildPlugins []*PluginInclude `json:"-"`
|
||||
Pinned bool `json:"-"`
|
||||
|
||||
Executable string `json:"executable,omitempty"`
|
||||
}
|
||||
|
||||
// AppPluginRoute describes a plugin route that is defined in
|
||||
// the plugin.json file for a plugin.
|
||||
type AppPluginRoute struct {
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
ReqRole models.RoleType `json:"reqRole"`
|
||||
URL string `json:"url"`
|
||||
URLParams []AppPluginRouteURLParam `json:"urlParams"`
|
||||
Headers []AppPluginRouteHeader `json:"headers"`
|
||||
AuthType string `json:"authType"`
|
||||
TokenAuth *JwtTokenAuth `json:"tokenAuth"`
|
||||
JwtTokenAuth *JwtTokenAuth `json:"jwtTokenAuth"`
|
||||
Body json.RawMessage `json:"body"`
|
||||
}
|
||||
|
||||
// AppPluginRouteHeader describes an HTTP header that is forwarded with
|
||||
// the proxied request for a plugin route
|
||||
type AppPluginRouteHeader struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// AppPluginRouteURLParam describes query string parameters for
|
||||
// a url in a plugin route
|
||||
type AppPluginRouteURLParam 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 (app *AppPlugin) Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) (
|
||||
interface{}, error) {
|
||||
if err := decoder.Decode(app); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if app.Backend {
|
||||
cmd := ComposePluginStartCommand(app.Executable)
|
||||
fullpath := filepath.Join(base.PluginDir, cmd)
|
||||
factory := grpcplugin.NewBackendPlugin(app.Id, fullpath)
|
||||
if err := backendPluginManager.RegisterAndStart(context.Background(), app.Id, factory); err != nil {
|
||||
return nil, errutil.Wrapf(err, "failed to register backend plugin")
|
||||
}
|
||||
}
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (app *AppPlugin) InitApp(panels map[string]*PanelPlugin, dataSources map[string]*DataSourcePlugin,
|
||||
cfg *setting.Cfg) []*PluginStaticRoute {
|
||||
staticRoutes := app.InitFrontendPlugin(cfg)
|
||||
|
||||
// check if we have child panels
|
||||
for _, panel := range panels {
|
||||
if strings.HasPrefix(panel.PluginDir, app.PluginDir) {
|
||||
panel.setPathsBasedOnApp(app, cfg)
|
||||
app.FoundChildPlugins = append(app.FoundChildPlugins, &PluginInclude{
|
||||
Name: panel.Name,
|
||||
Id: panel.Id,
|
||||
Type: panel.Type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// check if we have child datasources
|
||||
for _, ds := range dataSources {
|
||||
if strings.HasPrefix(ds.PluginDir, app.PluginDir) {
|
||||
ds.setPathsBasedOnApp(app, cfg)
|
||||
app.FoundChildPlugins = append(app.FoundChildPlugins, &PluginInclude{
|
||||
Name: ds.Name,
|
||||
Id: ds.Id,
|
||||
Type: ds.Type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// slugify pages
|
||||
for _, include := range app.Includes {
|
||||
if include.Slug == "" {
|
||||
include.Slug = slug.Make(include.Name)
|
||||
}
|
||||
if include.Type == "page" && include.DefaultNav {
|
||||
app.DefaultNavUrl = cfg.AppSubURL + "/plugins/" + app.Id + "/page/" + include.Slug
|
||||
}
|
||||
if include.Type == "dashboard" && include.DefaultNav {
|
||||
app.DefaultNavUrl = cfg.AppSubURL + include.GetSlugOrUIDLink()
|
||||
}
|
||||
}
|
||||
|
||||
return staticRoutes
|
||||
}
|
||||
@@ -17,3 +17,15 @@ func ComposePluginStartCommand(executable string) string {
|
||||
|
||||
return fmt.Sprintf("%s_%s_%s%s", executable, os, strings.ToLower(arch), extension)
|
||||
}
|
||||
|
||||
func ComposeRendererStartCommand() string {
|
||||
os := strings.ToLower(runtime.GOOS)
|
||||
arch := runtime.GOARCH
|
||||
extension := ""
|
||||
|
||||
if os == "windows" {
|
||||
extension = ".exe"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s_%s_%s%s", "plugin_start", os, strings.ToLower(arch), extension)
|
||||
}
|
||||
|
||||
@@ -5,33 +5,8 @@ import (
|
||||
|
||||
"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
|
||||
// RegisterAndStart registers and starts a backend plugin
|
||||
RegisterAndStart(ctx context.Context, pluginID string, factory PluginFactoryFunc) error
|
||||
// UnregisterAndStop unregisters and stops a backend plugin
|
||||
UnregisterAndStop(ctx context.Context, pluginID string) error
|
||||
// IsRegistered checks if a plugin is registered with the manager
|
||||
IsRegistered(pluginID string) bool
|
||||
// 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)
|
||||
// QueryData query data from a registered backend plugin.
|
||||
QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error)
|
||||
// CallResource calls a plugin resource.
|
||||
CallResource(pCtx backend.PluginContext, reqCtx *models.ReqContext, path string)
|
||||
// Get plugin by its ID.
|
||||
Get(pluginID string) (Plugin, bool)
|
||||
}
|
||||
|
||||
// Plugin is the backend plugin interface.
|
||||
type Plugin interface {
|
||||
PluginID() string
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
package instrumentation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@@ -68,19 +66,3 @@ func InstrumentCallResourceRequest(pluginID string, fn func() error) error {
|
||||
func InstrumentQueryDataRequest(pluginID string, fn func() error) error {
|
||||
return instrumentPluginRequest(pluginID, "queryData", fn)
|
||||
}
|
||||
|
||||
// InstrumentQueryDataHandler wraps a backend.QueryDataHandler with instrumentation of success rate and latency.
|
||||
func InstrumentQueryDataHandler(handler backend.QueryDataHandler) backend.QueryDataHandler {
|
||||
if handler == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return backend.QueryDataHandlerFunc(func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
var resp *backend.QueryDataResponse
|
||||
err := InstrumentQueryDataRequest(req.PluginContext.PluginID, func() (innerErr error) {
|
||||
resp, innerErr = handler.QueryData(ctx, req)
|
||||
return
|
||||
})
|
||||
return resp, err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,531 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
"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/plugins/backendplugin/instrumentation"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
"github.com/grafana/grafana/pkg/util/proxyutil"
|
||||
)
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, licensing models.Licensing,
|
||||
pluginRequestValidator models.PluginRequestValidator) *Manager {
|
||||
s := &Manager{
|
||||
Cfg: cfg,
|
||||
License: licensing,
|
||||
PluginRequestValidator: pluginRequestValidator,
|
||||
logger: log.New("plugins.backend"),
|
||||
plugins: map[string]backendplugin.Plugin{},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
Cfg *setting.Cfg
|
||||
License models.Licensing
|
||||
PluginRequestValidator models.PluginRequestValidator
|
||||
pluginsMu sync.RWMutex
|
||||
plugins map[string]backendplugin.Plugin
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (m *Manager) Run(ctx context.Context) error {
|
||||
<-ctx.Done()
|
||||
m.stop(ctx)
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// Register registers a backend plugin
|
||||
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()
|
||||
|
||||
if _, exists := m.plugins[pluginID]; exists {
|
||||
return fmt.Errorf("backend plugin %s already registered", pluginID)
|
||||
}
|
||||
|
||||
hostEnv := []string{
|
||||
fmt.Sprintf("GF_VERSION=%s", m.Cfg.BuildVersion),
|
||||
fmt.Sprintf("GF_EDITION=%s", m.License.Edition()),
|
||||
}
|
||||
|
||||
if m.License.HasLicense() {
|
||||
hostEnv = append(
|
||||
hostEnv,
|
||||
fmt.Sprintf("GF_ENTERPRISE_LICENSE_PATH=%s", m.Cfg.EnterpriseLicensePath),
|
||||
)
|
||||
|
||||
if envProvider, ok := m.License.(models.LicenseEnvironment); ok {
|
||||
for k, v := range envProvider.Environment() {
|
||||
hostEnv = append(hostEnv, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hostEnv = append(hostEnv, m.getAWSEnvironmentVariables()...)
|
||||
hostEnv = append(hostEnv, m.getAzureEnvironmentVariables()...)
|
||||
|
||||
pluginSettings := getPluginSettings(pluginID, m.Cfg)
|
||||
env := pluginSettings.ToEnv("GF_PLUGIN", hostEnv)
|
||||
|
||||
pluginLogger := m.logger.New("pluginId", pluginID)
|
||||
plugin, err := factory(pluginID, pluginLogger, env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.plugins[pluginID] = plugin
|
||||
m.logger.Debug("Backend plugin registered", "pluginId", pluginID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterAndStart registers and starts a backend plugin
|
||||
func (m *Manager) RegisterAndStart(ctx context.Context, pluginID string, factory backendplugin.PluginFactoryFunc) error {
|
||||
err := m.Register(pluginID, factory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p, exists := m.Get(pluginID)
|
||||
if !exists {
|
||||
return fmt.Errorf("backend plugin %s is not registered", pluginID)
|
||||
}
|
||||
|
||||
m.start(ctx, p)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnregisterAndStop unregisters and stops a backend plugin
|
||||
func (m *Manager) UnregisterAndStop(ctx context.Context, pluginID string) error {
|
||||
m.logger.Debug("Unregistering backend plugin", "pluginId", pluginID)
|
||||
m.pluginsMu.Lock()
|
||||
defer m.pluginsMu.Unlock()
|
||||
|
||||
p, exists := m.plugins[pluginID]
|
||||
if !exists {
|
||||
return fmt.Errorf("backend plugin %s is not registered", pluginID)
|
||||
}
|
||||
|
||||
m.logger.Debug("Stopping backend plugin process", "pluginId", pluginID)
|
||||
if err := p.Decommission(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.Stop(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(m.plugins, pluginID)
|
||||
|
||||
m.logger.Debug("Backend plugin unregistered", "pluginId", pluginID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) IsRegistered(pluginID string) bool {
|
||||
p, _ := m.Get(pluginID)
|
||||
|
||||
return p != nil && !p.IsDecommissioned()
|
||||
}
|
||||
|
||||
func (m *Manager) Get(pluginID string) (backendplugin.Plugin, bool) {
|
||||
m.pluginsMu.RLock()
|
||||
p, ok := m.plugins[pluginID]
|
||||
m.pluginsMu.RUnlock()
|
||||
|
||||
if ok && p.IsDecommissioned() {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return p, ok
|
||||
}
|
||||
|
||||
func (m *Manager) getAWSEnvironmentVariables() []string {
|
||||
variables := []string{}
|
||||
if m.Cfg.AWSAssumeRoleEnabled {
|
||||
variables = append(variables, awsds.AssumeRoleEnabledEnvVarKeyName+"=true")
|
||||
}
|
||||
if len(m.Cfg.AWSAllowedAuthProviders) > 0 {
|
||||
variables = append(variables, awsds.AllowedAuthProvidersEnvVarKeyName+"="+strings.Join(m.Cfg.AWSAllowedAuthProviders, ","))
|
||||
}
|
||||
|
||||
return variables
|
||||
}
|
||||
|
||||
func (m *Manager) getAzureEnvironmentVariables() []string {
|
||||
variables := []string{}
|
||||
if m.Cfg.Azure.Cloud != "" {
|
||||
variables = append(variables, "AZURE_CLOUD="+m.Cfg.Azure.Cloud)
|
||||
}
|
||||
if m.Cfg.Azure.ManagedIdentityClientId != "" {
|
||||
variables = append(variables, "AZURE_MANAGED_IDENTITY_CLIENT_ID="+m.Cfg.Azure.ManagedIdentityClientId)
|
||||
}
|
||||
if m.Cfg.Azure.ManagedIdentityEnabled {
|
||||
variables = append(variables, "AZURE_MANAGED_IDENTITY_ENABLED=true")
|
||||
}
|
||||
|
||||
return variables
|
||||
}
|
||||
|
||||
// start starts a managed backend plugin
|
||||
func (m *Manager) start(ctx context.Context, p backendplugin.Plugin) {
|
||||
if !p.IsManaged() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := startPluginAndRestartKilledProcesses(ctx, p); err != nil {
|
||||
p.Logger().Error("Failed to start plugin", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// StartPlugin starts a non-managed backend plugin
|
||||
func (m *Manager) StartPlugin(ctx context.Context, pluginID string) error {
|
||||
m.pluginsMu.RLock()
|
||||
p, registered := m.plugins[pluginID]
|
||||
m.pluginsMu.RUnlock()
|
||||
if !registered {
|
||||
return backendplugin.ErrPluginNotRegistered
|
||||
}
|
||||
|
||||
if p.IsManaged() {
|
||||
return errors.New("backend plugin is managed and cannot be manually started")
|
||||
}
|
||||
|
||||
return startPluginAndRestartKilledProcesses(ctx, p)
|
||||
}
|
||||
|
||||
// stop stops all managed backend plugins
|
||||
func (m *Manager) stop(ctx context.Context) {
|
||||
m.pluginsMu.RLock()
|
||||
defer m.pluginsMu.RUnlock()
|
||||
var wg sync.WaitGroup
|
||||
for _, p := range m.plugins {
|
||||
wg.Add(1)
|
||||
go func(p backendplugin.Plugin, ctx context.Context) {
|
||||
defer wg.Done()
|
||||
p.Logger().Debug("Stopping plugin")
|
||||
if err := p.Stop(ctx); err != nil {
|
||||
p.Logger().Error("Failed to stop plugin", "error", err)
|
||||
}
|
||||
p.Logger().Debug("Plugin stopped")
|
||||
}(p, ctx)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// CollectMetrics collects metrics from a registered backend plugin.
|
||||
func (m *Manager) CollectMetrics(ctx context.Context, pluginID string) (*backend.CollectMetricsResult, error) {
|
||||
p, registered := m.Get(pluginID)
|
||||
if !registered {
|
||||
return nil, backendplugin.ErrPluginNotRegistered
|
||||
}
|
||||
|
||||
var resp *backend.CollectMetricsResult
|
||||
err := instrumentation.InstrumentCollectMetrics(p.PluginID(), func() (innerErr error) {
|
||||
resp, innerErr = p.CollectMetrics(ctx)
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CheckHealth checks the health of a registered backend plugin.
|
||||
func (m *Manager) CheckHealth(ctx context.Context, pluginContext backend.PluginContext) (*backend.CheckHealthResult, error) {
|
||||
var dsURL string
|
||||
if pluginContext.DataSourceInstanceSettings != nil {
|
||||
dsURL = pluginContext.DataSourceInstanceSettings.URL
|
||||
}
|
||||
|
||||
err := m.PluginRequestValidator.Validate(dsURL, nil)
|
||||
if err != nil {
|
||||
return &backend.CheckHealthResult{
|
||||
Status: http.StatusForbidden,
|
||||
Message: "Access denied",
|
||||
}, nil
|
||||
}
|
||||
|
||||
p, registered := m.Get(pluginContext.PluginID)
|
||||
if !registered {
|
||||
return nil, backendplugin.ErrPluginNotRegistered
|
||||
}
|
||||
|
||||
var resp *backend.CheckHealthResult
|
||||
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, backendplugin.ErrMethodNotImplemented) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if errors.Is(err, backendplugin.ErrPluginUnavailable) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, errutil.Wrap("failed to check plugin health", backendplugin.ErrHealthCheckFailed)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *Manager) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
p, registered := m.Get(req.PluginContext.PluginID)
|
||||
if !registered {
|
||||
return nil, backendplugin.ErrPluginNotRegistered
|
||||
}
|
||||
|
||||
var resp *backend.QueryDataResponse
|
||||
err := instrumentation.InstrumentQueryDataRequest(p.PluginID(), func() (innerErr error) {
|
||||
resp, innerErr = p.QueryData(ctx, req)
|
||||
return
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if errors.Is(err, backendplugin.ErrPluginUnavailable) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, errutil.Wrap("failed to query data", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type keepCookiesJSONModel struct {
|
||||
KeepCookies []string `json:"keepCookies"`
|
||||
}
|
||||
|
||||
func (m *Manager) callResourceInternal(w http.ResponseWriter, req *http.Request, pCtx backend.PluginContext) error {
|
||||
p, registered := m.Get(pCtx.PluginID)
|
||||
if !registered {
|
||||
return backendplugin.ErrPluginNotRegistered
|
||||
}
|
||||
|
||||
keepCookieModel := keepCookiesJSONModel{}
|
||||
if dis := pCtx.DataSourceInstanceSettings; dis != nil {
|
||||
err := json.Unmarshal(dis.JSONData, &keepCookieModel)
|
||||
if err != nil {
|
||||
p.Logger().Error("Failed to to unpack JSONData in datasource instance settings", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
proxyutil.ClearCookieHeader(req, keepCookieModel.KeepCookies)
|
||||
proxyutil.PrepareProxyRequest(req)
|
||||
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read request body: %w", err)
|
||||
}
|
||||
|
||||
crReq := &backend.CallResourceRequest{
|
||||
PluginContext: pCtx,
|
||||
Path: req.URL.Path,
|
||||
Method: req.Method,
|
||||
URL: req.URL.String(),
|
||||
Headers: req.Header,
|
||||
Body: body,
|
||||
}
|
||||
|
||||
return instrumentation.InstrumentCallResourceRequest(p.PluginID(), func() error {
|
||||
childCtx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
stream := newCallResourceResponseStream(childCtx)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
defer func() {
|
||||
if err := stream.Close(); err != nil {
|
||||
m.logger.Warn("Failed to close stream", "err", err)
|
||||
}
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
var flushStreamErr error
|
||||
go func() {
|
||||
flushStreamErr = flushStream(p, stream, w)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
if err := p.CallResource(req.Context(), crReq, stream); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return flushStreamErr
|
||||
})
|
||||
}
|
||||
|
||||
// CallResource calls a plugin resource.
|
||||
func (m *Manager) CallResource(pCtx backend.PluginContext, reqCtx *models.ReqContext, path string) {
|
||||
var dsURL string
|
||||
if pCtx.DataSourceInstanceSettings != nil {
|
||||
dsURL = pCtx.DataSourceInstanceSettings.URL
|
||||
}
|
||||
|
||||
err := m.PluginRequestValidator.Validate(dsURL, reqCtx.Req)
|
||||
if err != nil {
|
||||
reqCtx.JsonApiErr(http.StatusForbidden, "Access denied", err)
|
||||
return
|
||||
}
|
||||
|
||||
clonedReq := reqCtx.Req.Clone(reqCtx.Req.Context())
|
||||
rawURL := path
|
||||
if clonedReq.URL.RawQuery != "" {
|
||||
rawURL += "?" + clonedReq.URL.RawQuery
|
||||
}
|
||||
urlPath, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
handleCallResourceError(err, reqCtx)
|
||||
return
|
||||
}
|
||||
clonedReq.URL = urlPath
|
||||
err = m.callResourceInternal(reqCtx.Resp, clonedReq, pCtx)
|
||||
if err != nil {
|
||||
handleCallResourceError(err, reqCtx)
|
||||
}
|
||||
}
|
||||
|
||||
func handleCallResourceError(err error, reqCtx *models.ReqContext) {
|
||||
if errors.Is(err, backendplugin.ErrPluginUnavailable) {
|
||||
reqCtx.JsonApiErr(503, "Plugin unavailable", err)
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
|
||||
reqCtx.JsonApiErr(404, "Not found", err)
|
||||
return
|
||||
}
|
||||
|
||||
reqCtx.JsonApiErr(500, "Failed to call resource", err)
|
||||
}
|
||||
|
||||
func flushStream(plugin backendplugin.Plugin, stream callResourceClientResponseStream, w http.ResponseWriter) error {
|
||||
processedStreams := 0
|
||||
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
if processedStreams == 0 {
|
||||
return errors.New("received empty resource response")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
if processedStreams == 0 {
|
||||
return errutil.Wrap("failed to receive response from resource call", err)
|
||||
}
|
||||
|
||||
plugin.Logger().Error("Failed to receive response from resource call", "error", err)
|
||||
return stream.Close()
|
||||
}
|
||||
|
||||
// Expected that headers and status are only part of first stream
|
||||
if processedStreams == 0 && resp.Headers != nil {
|
||||
// Make sure a content type always is returned in response
|
||||
if _, exists := resp.Headers["Content-Type"]; !exists {
|
||||
resp.Headers["Content-Type"] = []string{"application/json"}
|
||||
}
|
||||
|
||||
for k, values := range resp.Headers {
|
||||
// Due to security reasons we don't want to forward
|
||||
// cookies from a backend plugin to clients/browsers.
|
||||
if k == "Set-Cookie" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, v := range values {
|
||||
// TODO: Figure out if we should use Set here instead
|
||||
// nolint:gocritic
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(resp.Status)
|
||||
}
|
||||
|
||||
if _, err := w.Write(resp.Body); err != nil {
|
||||
plugin.Logger().Error("Failed to write resource response", "error", err)
|
||||
}
|
||||
|
||||
if flusher, ok := w.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
processedStreams++
|
||||
}
|
||||
}
|
||||
|
||||
func startPluginAndRestartKilledProcesses(ctx context.Context, p backendplugin.Plugin) error {
|
||||
if err := p.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}(ctx, p)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func restartKilledProcess(ctx context.Context, p backendplugin.Plugin) error {
|
||||
ticker := time.NewTicker(time.Second * 1)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
if p.IsDecommissioned() {
|
||||
p.Logger().Debug("Plugin decommissioned")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !p.Exited() {
|
||||
continue
|
||||
}
|
||||
|
||||
p.Logger().Debug("Restarting plugin")
|
||||
if err := p.Start(ctx); err != nil {
|
||||
p.Logger().Error("Failed to restart plugin", "error", err)
|
||||
continue
|
||||
}
|
||||
p.Logger().Debug("Plugin restarted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// callResourceClientResponseStream is used for receiving resource call responses.
|
||||
type callResourceClientResponseStream interface {
|
||||
Recv() (*backend.CallResourceResponse, error)
|
||||
Close() error
|
||||
}
|
||||
@@ -1,511 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
"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/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testPluginID = "test-plugin"
|
||||
|
||||
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, backendplugin.ErrPluginNotRegistered, err)
|
||||
|
||||
_, err = ctx.manager.CollectMetrics(context.Background(), testPluginID)
|
||||
require.Equal(t, backendplugin.ErrPluginNotRegistered, err)
|
||||
|
||||
_, err = ctx.manager.CheckHealth(context.Background(), backend.PluginContext{PluginID: testPluginID})
|
||||
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, backendplugin.ErrPluginNotRegistered, err)
|
||||
})
|
||||
})
|
||||
|
||||
newManagerScenario(t, true, func(t *testing.T, ctx *managerScenarioCtx) {
|
||||
t.Run("Managed plugin scenario", func(t *testing.T) {
|
||||
ctx.license.edition = "Open Source"
|
||||
ctx.license.hasLicense = false
|
||||
ctx.cfg.BuildVersion = "7.0.0"
|
||||
|
||||
t.Run("Should be able to register plugin", func(t *testing.T) {
|
||||
err := ctx.manager.RegisterAndStart(context.Background(), testPluginID, ctx.factory)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ctx.plugin)
|
||||
require.Equal(t, testPluginID, ctx.plugin.pluginID)
|
||||
require.NotNil(t, ctx.plugin.logger)
|
||||
require.Equal(t, 1, ctx.plugin.startCount)
|
||||
require.True(t, ctx.manager.IsRegistered(testPluginID))
|
||||
|
||||
t.Run("Should not be able to register an already registered plugin", func(t *testing.T) {
|
||||
err := ctx.manager.RegisterAndStart(context.Background(), testPluginID, ctx.factory)
|
||||
require.Equal(t, 1, ctx.plugin.startCount)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Should provide expected host environment variables", func(t *testing.T) {
|
||||
require.Len(t, ctx.env, 7)
|
||||
require.EqualValues(t, []string{
|
||||
"GF_VERSION=7.0.0",
|
||||
"GF_EDITION=Open Source",
|
||||
fmt.Sprintf("%s=true", awsds.AssumeRoleEnabledEnvVarKeyName),
|
||||
fmt.Sprintf("%s=keys,credentials", awsds.AllowedAuthProvidersEnvVarKeyName),
|
||||
"AZURE_CLOUD=AzureCloud",
|
||||
"AZURE_MANAGED_IDENTITY_CLIENT_ID=client-id",
|
||||
"AZURE_MANAGED_IDENTITY_ENABLED=true"},
|
||||
ctx.env)
|
||||
})
|
||||
|
||||
t.Run("When manager runs should start and stop plugin", func(t *testing.T) {
|
||||
pCtx := context.Background()
|
||||
cCtx, cancel := context.WithCancel(pCtx)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
var runErr error
|
||||
go func() {
|
||||
runErr = ctx.manager.Run(cCtx)
|
||||
wg.Done()
|
||||
}()
|
||||
time.Sleep(time.Millisecond)
|
||||
cancel()
|
||||
wg.Wait()
|
||||
require.Equal(t, context.Canceled, runErr)
|
||||
require.Equal(t, 1, ctx.plugin.startCount)
|
||||
require.Equal(t, 1, ctx.plugin.stopCount)
|
||||
})
|
||||
|
||||
t.Run("When manager runs should restart plugin process when killed", func(t *testing.T) {
|
||||
ctx.plugin.stopCount = 0
|
||||
ctx.plugin.startCount = 0
|
||||
pCtx := context.Background()
|
||||
cCtx, cancel := context.WithCancel(pCtx)
|
||||
var wgRun sync.WaitGroup
|
||||
wgRun.Add(1)
|
||||
var runErr error
|
||||
go func() {
|
||||
runErr = ctx.manager.Run(cCtx)
|
||||
wgRun.Done()
|
||||
}()
|
||||
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
var wgKill sync.WaitGroup
|
||||
wgKill.Add(1)
|
||||
go func() {
|
||||
ctx.plugin.kill()
|
||||
for {
|
||||
if !ctx.plugin.Exited() {
|
||||
break
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
wgKill.Done()
|
||||
}()
|
||||
wgKill.Wait()
|
||||
wgRun.Wait()
|
||||
require.Equal(t, context.Canceled, runErr)
|
||||
require.Equal(t, 1, ctx.plugin.stopCount)
|
||||
require.Equal(t, 1, ctx.plugin.startCount)
|
||||
})
|
||||
|
||||
t.Run("Shouldn't be able to start managed plugin", func(t *testing.T) {
|
||||
err := ctx.manager.StartPlugin(context.Background(), testPluginID)
|
||||
require.NotNil(t, err)
|
||||
})
|
||||
|
||||
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, 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, backendplugin.ErrMethodNotImplemented, err)
|
||||
})
|
||||
|
||||
t.Run("Call resource should return method not implemented error", func(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, "/test", bytes.NewReader([]byte{}))
|
||||
require.NoError(t, err)
|
||||
w := httptest.NewRecorder()
|
||||
err = ctx.manager.callResourceInternal(w, req, backend.PluginContext{PluginID: testPluginID})
|
||||
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 = 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)
|
||||
require.NotNil(t, res)
|
||||
require.Equal(t, "hello", string(res.PrometheusMetrics))
|
||||
})
|
||||
|
||||
t.Run("Check health should return expected result", func(t *testing.T) {
|
||||
json := []byte(`{
|
||||
"key": "value"
|
||||
}`)
|
||||
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)
|
||||
require.NotNil(t, res)
|
||||
require.Equal(t, backend.HealthStatusOk, res.Status)
|
||||
require.Equal(t, "All good", res.Message)
|
||||
require.Equal(t, json, res.JSONDetails)
|
||||
})
|
||||
|
||||
t.Run("Call resource should return expected response", func(t *testing.T) {
|
||||
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)
|
||||
w := httptest.NewRecorder()
|
||||
err = ctx.manager.callResourceInternal(w, req, backend.PluginContext{PluginID: testPluginID})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Should be able to decommission a running plugin", func(t *testing.T) {
|
||||
require.True(t, ctx.manager.IsRegistered(testPluginID))
|
||||
|
||||
err := ctx.manager.UnregisterAndStop(context.Background(), testPluginID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 2, ctx.plugin.stopCount)
|
||||
require.False(t, ctx.manager.IsRegistered(testPluginID))
|
||||
p := ctx.manager.plugins[testPluginID]
|
||||
require.Nil(t, p)
|
||||
|
||||
err = ctx.manager.StartPlugin(context.Background(), testPluginID)
|
||||
require.Equal(t, backendplugin.ErrPluginNotRegistered, err)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
newManagerScenario(t, false, func(t *testing.T, ctx *managerScenarioCtx) {
|
||||
t.Run("Unmanaged plugin scenario", func(t *testing.T) {
|
||||
ctx.license.edition = "Open Source"
|
||||
ctx.license.hasLicense = false
|
||||
ctx.cfg.BuildVersion = "7.0.0"
|
||||
|
||||
t.Run("Should be able to register plugin", func(t *testing.T) {
|
||||
err := ctx.manager.RegisterAndStart(context.Background(), testPluginID, ctx.factory)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ctx.manager.IsRegistered(testPluginID))
|
||||
require.False(t, ctx.plugin.managed)
|
||||
|
||||
t.Run("When manager runs should not start plugin", func(t *testing.T) {
|
||||
pCtx := context.Background()
|
||||
cCtx, cancel := context.WithCancel(pCtx)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
var runErr error
|
||||
go func() {
|
||||
runErr = ctx.manager.Run(cCtx)
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
cancel()
|
||||
}()
|
||||
wg.Wait()
|
||||
require.Equal(t, context.Canceled, runErr)
|
||||
require.Equal(t, 0, ctx.plugin.startCount)
|
||||
require.Equal(t, 1, ctx.plugin.stopCount)
|
||||
})
|
||||
|
||||
t.Run("Should be able to start unmanaged plugin and be restarted when process is killed", func(t *testing.T) {
|
||||
pCtx := context.Background()
|
||||
cCtx, cancel := context.WithCancel(pCtx)
|
||||
defer cancel()
|
||||
err := ctx.manager.StartPlugin(cCtx, testPluginID)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, ctx.plugin.startCount)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
ctx.plugin.kill()
|
||||
for {
|
||||
if !ctx.plugin.Exited() {
|
||||
break
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
require.Equal(t, 2, ctx.plugin.startCount)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
newManagerScenario(t, true, func(t *testing.T, ctx *managerScenarioCtx) {
|
||||
t.Run("Plugin registration scenario when Grafana is licensed", func(t *testing.T) {
|
||||
ctx.license.edition = "Enterprise"
|
||||
ctx.license.hasLicense = true
|
||||
ctx.license.tokenRaw = "testtoken"
|
||||
ctx.cfg.BuildVersion = "7.0.0"
|
||||
ctx.cfg.EnterpriseLicensePath = "/license.txt"
|
||||
|
||||
err := ctx.manager.RegisterAndStart(context.Background(), testPluginID, ctx.factory)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("Should provide expected host environment variables", func(t *testing.T) {
|
||||
require.Len(t, ctx.env, 9)
|
||||
require.EqualValues(t, []string{
|
||||
"GF_VERSION=7.0.0",
|
||||
"GF_EDITION=Enterprise",
|
||||
"GF_ENTERPRISE_LICENSE_PATH=/license.txt",
|
||||
"GF_ENTERPRISE_LICENSE_TEXT=testtoken",
|
||||
fmt.Sprintf("%s=true", awsds.AssumeRoleEnabledEnvVarKeyName),
|
||||
fmt.Sprintf("%s=keys,credentials", awsds.AllowedAuthProvidersEnvVarKeyName),
|
||||
"AZURE_CLOUD=AzureCloud",
|
||||
"AZURE_MANAGED_IDENTITY_CLIENT_ID=client-id",
|
||||
"AZURE_MANAGED_IDENTITY_ENABLED=true"},
|
||||
ctx.env)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type managerScenarioCtx struct {
|
||||
cfg *setting.Cfg
|
||||
license *testLicensingService
|
||||
manager *Manager
|
||||
factory backendplugin.PluginFactoryFunc
|
||||
plugin *testPlugin
|
||||
env []string
|
||||
}
|
||||
|
||||
func newManagerScenario(t *testing.T, managed bool, fn func(t *testing.T, ctx *managerScenarioCtx)) {
|
||||
t.Helper()
|
||||
cfg := setting.NewCfg()
|
||||
cfg.AWSAllowedAuthProviders = []string{"keys", "credentials"}
|
||||
cfg.AWSAssumeRoleEnabled = true
|
||||
|
||||
cfg.Azure.ManagedIdentityEnabled = true
|
||||
cfg.Azure.Cloud = "AzureCloud"
|
||||
cfg.Azure.ManagedIdentityClientId = "client-id"
|
||||
|
||||
license := &testLicensingService{}
|
||||
validator := &testPluginRequestValidator{}
|
||||
ctx := &managerScenarioCtx{
|
||||
cfg: cfg,
|
||||
license: license,
|
||||
manager: &Manager{
|
||||
Cfg: cfg,
|
||||
License: license,
|
||||
PluginRequestValidator: validator,
|
||||
logger: log.New("test"),
|
||||
plugins: map[string]backendplugin.Plugin{},
|
||||
},
|
||||
}
|
||||
|
||||
ctx.factory = func(pluginID string, logger log.Logger, env []string) (backendplugin.Plugin, error) {
|
||||
ctx.plugin = &testPlugin{
|
||||
pluginID: pluginID,
|
||||
logger: logger,
|
||||
managed: managed,
|
||||
}
|
||||
ctx.env = env
|
||||
|
||||
return ctx.plugin, nil
|
||||
}
|
||||
|
||||
fn(t, ctx)
|
||||
}
|
||||
|
||||
type testPlugin struct {
|
||||
pluginID string
|
||||
logger log.Logger
|
||||
startCount int
|
||||
stopCount int
|
||||
managed bool
|
||||
exited bool
|
||||
decommissioned bool
|
||||
backend.CollectMetricsHandlerFunc
|
||||
backend.CheckHealthHandlerFunc
|
||||
backend.QueryDataHandlerFunc
|
||||
backend.CallResourceHandlerFunc
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (tp *testPlugin) PluginID() string {
|
||||
return tp.pluginID
|
||||
}
|
||||
|
||||
func (tp *testPlugin) Logger() log.Logger {
|
||||
return tp.logger
|
||||
}
|
||||
|
||||
func (tp *testPlugin) Start(ctx context.Context) error {
|
||||
tp.mutex.Lock()
|
||||
defer tp.mutex.Unlock()
|
||||
tp.exited = false
|
||||
tp.startCount++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tp *testPlugin) Stop(ctx context.Context) error {
|
||||
tp.mutex.Lock()
|
||||
defer tp.mutex.Unlock()
|
||||
tp.stopCount++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tp *testPlugin) IsManaged() bool {
|
||||
return tp.managed
|
||||
}
|
||||
|
||||
func (tp *testPlugin) Exited() bool {
|
||||
tp.mutex.RLock()
|
||||
defer tp.mutex.RUnlock()
|
||||
return tp.exited
|
||||
}
|
||||
|
||||
func (tp *testPlugin) Decommission() error {
|
||||
tp.mutex.Lock()
|
||||
defer tp.mutex.Unlock()
|
||||
|
||||
tp.decommissioned = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tp *testPlugin) IsDecommissioned() bool {
|
||||
tp.mutex.RLock()
|
||||
defer tp.mutex.RUnlock()
|
||||
return tp.decommissioned
|
||||
}
|
||||
|
||||
func (tp *testPlugin) kill() {
|
||||
tp.mutex.Lock()
|
||||
defer tp.mutex.Unlock()
|
||||
tp.exited = true
|
||||
}
|
||||
|
||||
func (tp *testPlugin) CollectMetrics(ctx context.Context) (*backend.CollectMetricsResult, error) {
|
||||
if tp.CollectMetricsHandlerFunc != nil {
|
||||
return tp.CollectMetricsHandlerFunc(ctx)
|
||||
}
|
||||
|
||||
return nil, backendplugin.ErrMethodNotImplemented
|
||||
}
|
||||
|
||||
func (tp *testPlugin) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
if tp.CheckHealthHandlerFunc != nil {
|
||||
return tp.CheckHealthHandlerFunc(ctx, req)
|
||||
}
|
||||
|
||||
return nil, backendplugin.ErrMethodNotImplemented
|
||||
}
|
||||
|
||||
func (tp *testPlugin) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
if tp.QueryDataHandlerFunc != nil {
|
||||
return tp.QueryDataHandlerFunc(ctx, req)
|
||||
}
|
||||
|
||||
return nil, backendplugin.ErrMethodNotImplemented
|
||||
}
|
||||
|
||||
func (tp *testPlugin) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
if tp.CallResourceHandlerFunc != nil {
|
||||
return tp.CallResourceHandlerFunc(ctx, req, sender)
|
||||
}
|
||||
|
||||
return backendplugin.ErrMethodNotImplemented
|
||||
}
|
||||
|
||||
func (tp *testPlugin) SubscribeStream(ctx context.Context, request *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
|
||||
return nil, backendplugin.ErrMethodNotImplemented
|
||||
}
|
||||
|
||||
func (tp *testPlugin) PublishStream(ctx context.Context, request *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
|
||||
return nil, backendplugin.ErrMethodNotImplemented
|
||||
}
|
||||
|
||||
func (tp *testPlugin) RunStream(ctx context.Context, request *backend.RunStreamRequest, sender *backend.StreamSender) error {
|
||||
return backendplugin.ErrMethodNotImplemented
|
||||
}
|
||||
|
||||
type testLicensingService struct {
|
||||
edition string
|
||||
hasLicense bool
|
||||
tokenRaw string
|
||||
}
|
||||
|
||||
func (t *testLicensingService) HasLicense() bool {
|
||||
return t.hasLicense
|
||||
}
|
||||
|
||||
func (t *testLicensingService) Expiry() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (t *testLicensingService) Edition() string {
|
||||
return t.edition
|
||||
}
|
||||
|
||||
func (t *testLicensingService) StateInfo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *testLicensingService) ContentDeliveryPrefix() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *testLicensingService) LicenseURL(showAdminLicensingPage bool) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *testLicensingService) HasValidLicense() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *testLicensingService) Environment() map[string]string {
|
||||
return map[string]string{"GF_ENTERPRISE_LICENSE_TEXT": t.tokenRaw}
|
||||
}
|
||||
|
||||
type testPluginRequestValidator struct{}
|
||||
|
||||
func (t *testPluginRequestValidator) Validate(string, *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type pluginSettings map[string]string
|
||||
|
||||
func (ps pluginSettings) ToEnv(prefix string, hostEnv []string) []string {
|
||||
var env []string
|
||||
for k, v := range ps {
|
||||
key := fmt.Sprintf("%s_%s", prefix, strings.ToUpper(k))
|
||||
if value := os.Getenv(key); value != "" {
|
||||
v = value
|
||||
}
|
||||
|
||||
env = append(env, fmt.Sprintf("%s=%s", key, v))
|
||||
}
|
||||
|
||||
env = append(env, hostEnv...)
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
func getPluginSettings(plugID string, cfg *setting.Cfg) pluginSettings {
|
||||
ps := pluginSettings{}
|
||||
for k, v := range cfg.PluginSettings[plugID] {
|
||||
if k == "path" || strings.ToLower(k) == "id" {
|
||||
continue
|
||||
}
|
||||
|
||||
ps[k] = v
|
||||
}
|
||||
|
||||
return ps
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPluginSettings(t *testing.T) {
|
||||
t.Run("Should only extract from sections beginning with 'plugin.' in config", func(t *testing.T) {
|
||||
cfg := &setting.Cfg{
|
||||
PluginSettings: setting.PluginSettings{
|
||||
"plugin": map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ps := getPluginSettings("plugin", cfg)
|
||||
require.Len(t, ps, 2)
|
||||
|
||||
t.Run("Should skip path setting", func(t *testing.T) {
|
||||
cfg.PluginSettings["plugin"]["path"] = "value"
|
||||
ps := getPluginSettings("plugin", cfg)
|
||||
require.Len(t, ps, 2)
|
||||
})
|
||||
|
||||
t.Run("Should skip id setting", func(t *testing.T) {
|
||||
cfg.PluginSettings["plugin"]["id"] = "value"
|
||||
ps := getPluginSettings("plugin", cfg)
|
||||
require.Len(t, ps, 2)
|
||||
})
|
||||
|
||||
t.Run("Should return expected environment variables from plugin settings ", func(t *testing.T) {
|
||||
ps := getPluginSettings("plugin", cfg)
|
||||
env := ps.ToEnv("GF_PLUGIN", []string{"GF_VERSION=6.7.0"})
|
||||
sort.Strings(env)
|
||||
require.Len(t, env, 3)
|
||||
require.EqualValues(t, []string{"GF_PLUGIN_KEY1=value1", "GF_PLUGIN_KEY2=value2", "GF_VERSION=6.7.0"}, env)
|
||||
})
|
||||
|
||||
t.Run("Should override config variable with environment variable ", func(t *testing.T) {
|
||||
_ = os.Setenv("GF_PLUGIN_KEY1", "sth")
|
||||
t.Cleanup(func() {
|
||||
_ = os.Unsetenv("GF_PLUGIN_KEY1")
|
||||
})
|
||||
|
||||
ps := getPluginSettings("plugin", cfg)
|
||||
env := ps.ToEnv("GF_PLUGIN", []string{"GF_VERSION=6.7.0"})
|
||||
sort.Strings(env)
|
||||
require.Len(t, env, 3)
|
||||
require.EqualValues(t, []string{"GF_PLUGIN_KEY1=sth", "GF_PLUGIN_KEY2=value2", "GF_VERSION=6.7.0"}, env)
|
||||
})
|
||||
|
||||
t.Run("Config variable doesn't match env variable ", func(t *testing.T) {
|
||||
_ = os.Setenv("GF_PLUGIN_KEY3", "value3")
|
||||
t.Cleanup(func() {
|
||||
_ = os.Unsetenv("GF_PLUGIN_KEY3")
|
||||
})
|
||||
|
||||
ps := getPluginSettings("plugin", cfg)
|
||||
env := ps.ToEnv("GF_PLUGIN", []string{"GF_VERSION=6.7.0"})
|
||||
sort.Strings(env)
|
||||
require.Len(t, env, 3)
|
||||
require.EqualValues(t, []string{"GF_PLUGIN_KEY1=value1", "GF_PLUGIN_KEY2=value2", "GF_VERSION=6.7.0"}, env)
|
||||
})
|
||||
|
||||
t.Run("Should override missing config variable with environment variable ", func(t *testing.T) {
|
||||
cfg := &setting.Cfg{
|
||||
PluginSettings: setting.PluginSettings{
|
||||
"plugin": map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ps := getPluginSettings("plugin", cfg)
|
||||
require.Len(t, ps, 2)
|
||||
|
||||
_ = os.Setenv("GF_PLUGIN_KEY2", "sth")
|
||||
t.Cleanup(func() {
|
||||
_ = os.Unsetenv("GF_PLUGIN_KEY1")
|
||||
})
|
||||
|
||||
env := ps.ToEnv("GF_PLUGIN", []string{"GF_VERSION=6.7.0"})
|
||||
sort.Strings(env)
|
||||
require.Len(t, env, 3)
|
||||
require.EqualValues(t, []string{"GF_PLUGIN_KEY1=value1", "GF_PLUGIN_KEY2=sth", "GF_VERSION=6.7.0"}, env)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
)
|
||||
|
||||
func newCallResourceResponseStream(ctx context.Context) *callResourceResponseStream {
|
||||
return &callResourceResponseStream{
|
||||
ctx: ctx,
|
||||
stream: make(chan *backend.CallResourceResponse),
|
||||
}
|
||||
}
|
||||
|
||||
type callResourceResponseStream struct {
|
||||
ctx context.Context
|
||||
stream chan *backend.CallResourceResponse
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (s *callResourceResponseStream) Send(res *backend.CallResourceResponse) error {
|
||||
if s.closed {
|
||||
return errors.New("cannot send to a closed stream")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return errors.New("cancelled")
|
||||
case s.stream <- res:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *callResourceResponseStream) Recv() (*backend.CallResourceResponse, error) {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return nil, s.ctx.Err()
|
||||
case res, ok := <-s.stream:
|
||||
if !ok {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *callResourceResponseStream) Close() error {
|
||||
if s.closed {
|
||||
return errors.New("cannot close a closed stream")
|
||||
}
|
||||
|
||||
close(s.stream)
|
||||
s.closed = true
|
||||
return nil
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/grpcplugin"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
// DataSourcePlugin contains all metadata about a datasource plugin
|
||||
type DataSourcePlugin struct {
|
||||
FrontendPluginBase
|
||||
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"`
|
||||
Routes []*AppPluginRoute `json:"routes"`
|
||||
Streaming bool `json:"streaming"`
|
||||
|
||||
Backend bool `json:"backend,omitempty"`
|
||||
Executable string `json:"executable,omitempty"`
|
||||
SDK bool `json:"sdk,omitempty"`
|
||||
}
|
||||
|
||||
func (p *DataSourcePlugin) Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) (
|
||||
interface{}, error) {
|
||||
if err := decoder.Decode(p); err != nil {
|
||||
return nil, errutil.Wrapf(err, "Failed to decode datasource plugin")
|
||||
}
|
||||
|
||||
if p.Backend {
|
||||
cmd := ComposePluginStartCommand(p.Executable)
|
||||
fullpath := filepath.Join(base.PluginDir, cmd)
|
||||
factory := grpcplugin.NewBackendPlugin(p.Id, fullpath)
|
||||
if err := backendPluginManager.RegisterAndStart(context.Background(), p.Id, factory); err != nil {
|
||||
return nil, errutil.Wrapf(err, "failed to register backend plugin")
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package plugins
|
||||
|
||||
type ErrorCode string
|
||||
|
||||
type PluginError struct {
|
||||
ErrorCode `json:"errorCode"`
|
||||
PluginID string `json:"pluginId,omitempty"`
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type FrontendPluginBase struct {
|
||||
PluginBase
|
||||
}
|
||||
|
||||
func (fp *FrontendPluginBase) InitFrontendPlugin(cfg *setting.Cfg) []*PluginStaticRoute {
|
||||
var staticRoutes []*PluginStaticRoute
|
||||
if isExternalPlugin(fp.PluginDir, cfg) {
|
||||
staticRoutes = []*PluginStaticRoute{
|
||||
{
|
||||
Directory: fp.PluginDir,
|
||||
PluginId: fp.Id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fp.handleModuleDefaults(cfg)
|
||||
|
||||
fp.Info.Logos.Small = getPluginLogoUrl(fp.Type, fp.Info.Logos.Small, fp.BaseUrl)
|
||||
fp.Info.Logos.Large = getPluginLogoUrl(fp.Type, fp.Info.Logos.Large, fp.BaseUrl)
|
||||
|
||||
for i := 0; i < len(fp.Info.Screenshots); i++ {
|
||||
fp.Info.Screenshots[i].Path = evalRelativePluginUrlPath(fp.Info.Screenshots[i].Path, fp.BaseUrl, fp.Type)
|
||||
}
|
||||
|
||||
return staticRoutes
|
||||
}
|
||||
|
||||
func getPluginLogoUrl(pluginType, path, baseUrl string) string {
|
||||
if path == "" {
|
||||
return defaultLogoPath(pluginType)
|
||||
}
|
||||
|
||||
return evalRelativePluginUrlPath(path, baseUrl, pluginType)
|
||||
}
|
||||
|
||||
func defaultLogoPath(pluginType string) string {
|
||||
return "public/img/icn-" + pluginType + ".svg"
|
||||
}
|
||||
|
||||
func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin, cfg *setting.Cfg) {
|
||||
appSubPath := strings.ReplaceAll(strings.Replace(fp.PluginDir, app.PluginDir, "", 1), "\\", "/")
|
||||
fp.IncludedInAppId = app.Id
|
||||
fp.BaseUrl = app.BaseUrl
|
||||
|
||||
if isExternalPlugin(app.PluginDir, cfg) {
|
||||
fp.Module = util.JoinURLFragments("plugins/"+app.Id, appSubPath) + "/module"
|
||||
} else {
|
||||
fp.Module = util.JoinURLFragments("app/plugins/app/"+app.Id, appSubPath) + "/module"
|
||||
}
|
||||
}
|
||||
|
||||
func (fp *FrontendPluginBase) handleModuleDefaults(cfg *setting.Cfg) {
|
||||
if isExternalPlugin(fp.PluginDir, cfg) {
|
||||
fp.Module = path.Join("plugins", fp.Id, "module")
|
||||
fp.BaseUrl = path.Join("public/plugins", fp.Id)
|
||||
return
|
||||
}
|
||||
|
||||
fp.IsCorePlugin = true
|
||||
// Previously there was an assumption that the plugin directory
|
||||
// should be public/app/plugins/<plugin type>/<plugin id>
|
||||
// However this can be an issue if the plugin directory should be renamed to something else
|
||||
currentDir := filepath.Base(fp.PluginDir)
|
||||
// use path package for the following statements
|
||||
// because these are not file paths
|
||||
fp.Module = path.Join("app/plugins", fp.Type, currentDir, "module")
|
||||
fp.BaseUrl = path.Join("public/app/plugins", fp.Type, currentDir)
|
||||
}
|
||||
|
||||
func isExternalPlugin(pluginDir string, cfg *setting.Cfg) bool {
|
||||
return !strings.Contains(pluginDir, cfg.StaticRootPath)
|
||||
}
|
||||
|
||||
func evalRelativePluginUrlPath(pathStr, baseUrl, pluginType string) string {
|
||||
if pathStr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
u, _ := url.Parse(pathStr)
|
||||
if u.IsAbs() {
|
||||
return pathStr
|
||||
}
|
||||
|
||||
// is set as default or has already been prefixed with base path
|
||||
if pathStr == defaultLogoPath(pluginType) || strings.HasPrefix(pathStr, baseUrl) {
|
||||
return pathStr
|
||||
}
|
||||
|
||||
return path.Join(baseUrl, pathStr)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFrontendPlugin(t *testing.T) {
|
||||
t.Run("When setting paths based on App on Windows", func(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.StaticRootPath = "c:\\grafana\\public"
|
||||
|
||||
fp := &FrontendPluginBase{
|
||||
PluginBase: PluginBase{
|
||||
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata\\datasources\\datasource",
|
||||
BaseUrl: "fpbase",
|
||||
},
|
||||
}
|
||||
app := &AppPlugin{
|
||||
FrontendPluginBase: FrontendPluginBase{
|
||||
PluginBase: PluginBase{
|
||||
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata",
|
||||
Id: "testdata",
|
||||
BaseUrl: "public/app/plugins/app/testdata",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fp.setPathsBasedOnApp(app, cfg)
|
||||
require.Equal(t, "app/plugins/app/testdata/datasources/datasource/module", fp.Module)
|
||||
})
|
||||
}
|
||||
+88
-75
@@ -3,60 +3,102 @@ package plugins
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
)
|
||||
|
||||
// Manager is the plugin manager service interface.
|
||||
type Manager interface {
|
||||
// Renderer gets the renderer plugin.
|
||||
Renderer() *RendererPlugin
|
||||
// GetDataSource gets a data source plugin with a certain ID.
|
||||
GetDataSource(id string) *DataSourcePlugin
|
||||
// GetPlugin gets a plugin with a certain ID.
|
||||
GetPlugin(id string) *PluginBase
|
||||
// GetApp gets an app plugin with a certain ID.
|
||||
GetApp(id string) *AppPlugin
|
||||
// DataSourceCount gets the number of data sources.
|
||||
DataSourceCount() int
|
||||
// DataSources gets all data sources.
|
||||
DataSources() []*DataSourcePlugin
|
||||
// Apps gets all app plugins.
|
||||
Apps() []*AppPlugin
|
||||
// PanelCount gets the number of panels.
|
||||
PanelCount() int
|
||||
// AppCount gets the number of apps.
|
||||
AppCount() int
|
||||
// GetEnabledPlugins gets enabled plugins.
|
||||
GetEnabledPlugins(orgID int64) (*EnabledPlugins, error)
|
||||
// GrafanaLatestVersion gets the latest Grafana version.
|
||||
GrafanaLatestVersion() string
|
||||
// GrafanaHasUpdate returns whether Grafana has an update.
|
||||
GrafanaHasUpdate() bool
|
||||
// Plugins gets all plugins.
|
||||
Plugins() []*PluginBase
|
||||
// StaticRoutes gets all static routes.
|
||||
StaticRoutes() []*PluginStaticRoute
|
||||
// GetPluginSettings gets settings for a certain plugin.
|
||||
GetPluginSettings(orgID int64) (map[string]*models.PluginSettingInfoDTO, error)
|
||||
// DataRequestHandler is a data request handler interface.
|
||||
type DataRequestHandler interface {
|
||||
// HandleRequest handles a data request.
|
||||
HandleRequest(context.Context, *models.DataSource, DataQuery) (DataResponse, error)
|
||||
}
|
||||
|
||||
// Store is the storage for plugins.
|
||||
type Store interface {
|
||||
// Plugin finds a plugin by its ID.
|
||||
Plugin(pluginID string) *Plugin
|
||||
// Plugins returns plugins by their requested type.
|
||||
Plugins(pluginTypes ...Type) []*Plugin
|
||||
|
||||
// Add adds a plugin to the store.
|
||||
Add(ctx context.Context, pluginID, version string, opts AddOpts) error
|
||||
// Remove removes a plugin from the store.
|
||||
Remove(ctx context.Context, pluginID string) error
|
||||
}
|
||||
|
||||
type AddOpts struct {
|
||||
PluginInstallDir, PluginZipURL, PluginRepoURL string
|
||||
}
|
||||
|
||||
// Loader is responsible for loading plugins from the file system.
|
||||
type Loader interface {
|
||||
// Load will return a list of plugins found in the provided file system paths.
|
||||
Load(paths []string, ignore map[string]struct{}) ([]*Plugin, error)
|
||||
// LoadWithFactory will return a plugin found in the provided file system path and use the provided factory to
|
||||
// construct the plugin backend client.
|
||||
LoadWithFactory(path string, factory backendplugin.PluginFactoryFunc) (*Plugin, error)
|
||||
}
|
||||
|
||||
// Installer is responsible for managing plugins (add / remove) on the file system.
|
||||
type Installer interface {
|
||||
// Install downloads the requested plugin in the provided file system location.
|
||||
Install(ctx context.Context, pluginID, version, pluginsDir, pluginZipURL, pluginRepoURL string) error
|
||||
// Uninstall removes the requested plugin from the provided file system location.
|
||||
Uninstall(ctx context.Context, pluginDir string) error
|
||||
// GetUpdateInfo provides update information for the requested plugin.
|
||||
GetUpdateInfo(ctx context.Context, pluginID, version, pluginRepoURL string) (UpdateInfo, error)
|
||||
}
|
||||
|
||||
type UpdateInfo struct {
|
||||
PluginZipURL string
|
||||
}
|
||||
|
||||
// Client is used to communicate with backend plugin implementations.
|
||||
type Client interface {
|
||||
backend.QueryDataHandler
|
||||
backend.CheckHealthHandler
|
||||
|
||||
// CallResource calls a plugin resource.
|
||||
CallResource(pCtx backend.PluginContext, ctx *models.ReqContext, path string)
|
||||
// CollectMetrics collects metrics from a plugin.
|
||||
CollectMetrics(ctx context.Context, pluginID string) (*backend.CollectMetricsResult, error)
|
||||
}
|
||||
|
||||
type RendererManager interface {
|
||||
// Renderer returns a renderer plugin.
|
||||
Renderer() *Plugin
|
||||
}
|
||||
|
||||
type CoreBackendRegistrar interface {
|
||||
// LoadAndRegister loads and registers a Core backend plugin
|
||||
LoadAndRegister(pluginID string, factory backendplugin.PluginFactoryFunc) error
|
||||
}
|
||||
|
||||
type StaticRouteResolver interface {
|
||||
Routes() []*StaticRoute
|
||||
}
|
||||
|
||||
type ErrorResolver interface {
|
||||
PluginErrors() []*Error
|
||||
}
|
||||
|
||||
type PluginLoaderAuthorizer interface {
|
||||
// CanLoadPlugin confirms if a plugin is authorized to load
|
||||
CanLoadPlugin(plugin *Plugin) bool
|
||||
}
|
||||
|
||||
type PluginDashboardManager interface {
|
||||
// GetPluginDashboards gets dashboards for a certain org/plugin.
|
||||
GetPluginDashboards(orgID int64, pluginID string) ([]*PluginDashboardInfoDTO, error)
|
||||
// GetPluginMarkdown gets markdown for a certain plugin/name.
|
||||
GetPluginMarkdown(pluginID string, name string) ([]byte, error)
|
||||
// ImportDashboard imports a dashboard.
|
||||
ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json,
|
||||
overwrite bool, inputs []ImportDashboardInput, user *models.SignedInUser,
|
||||
requestHandler DataRequestHandler) (PluginDashboardInfoDTO, *models.Dashboard, error)
|
||||
// ScanningErrors returns plugin scanning errors encountered.
|
||||
ScanningErrors() []PluginError
|
||||
// LoadPluginDashboard loads a plugin dashboard.
|
||||
LoadPluginDashboard(pluginID, path string) (*models.Dashboard, error)
|
||||
// IsAppInstalled returns whether an app is installed.
|
||||
IsAppInstalled(id string) bool
|
||||
// Install installs a plugin.
|
||||
Install(ctx context.Context, pluginID, version string) error
|
||||
// Uninstall uninstalls a plugin.
|
||||
Uninstall(ctx context.Context, pluginID string) error
|
||||
// ImportDashboard imports a dashboard.
|
||||
ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json,
|
||||
overwrite bool, inputs []ImportDashboardInput, user *models.SignedInUser) (PluginDashboardInfoDTO,
|
||||
*models.Dashboard, error)
|
||||
}
|
||||
|
||||
type ImportDashboardInput struct {
|
||||
@@ -65,32 +107,3 @@ type ImportDashboardInput struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// DataRequestHandler is a data request handler interface.
|
||||
type DataRequestHandler interface {
|
||||
// HandleRequest handles a data request.
|
||||
HandleRequest(context.Context, *models.DataSource, DataQuery) (DataResponse, error)
|
||||
}
|
||||
|
||||
type PluginInstaller interface {
|
||||
// Install finds the plugin given the provided information and installs in the provided plugins directory.
|
||||
Install(ctx context.Context, pluginID, version, pluginsDirectory, pluginZipURL, pluginRepoURL string) error
|
||||
// Uninstall removes the specified plugin from the provided plugins directory.
|
||||
Uninstall(ctx context.Context, pluginPath string) error
|
||||
// GetUpdateInfo returns update information if the requested plugin is supported on the running system.
|
||||
GetUpdateInfo(pluginID, version, pluginRepoURL string) (UpdateInfo, error)
|
||||
}
|
||||
|
||||
type PluginInstallerLogger interface {
|
||||
Successf(format string, args ...interface{})
|
||||
Failuref(format string, args ...interface{})
|
||||
|
||||
Info(args ...interface{})
|
||||
Infof(format string, args ...interface{})
|
||||
Debug(args ...interface{})
|
||||
Debugf(format string, args ...interface{})
|
||||
Warn(args ...interface{})
|
||||
Warnf(format string, args ...interface{})
|
||||
Error(args ...interface{})
|
||||
Errorf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ import (
|
||||
"regexp"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var varRegex = regexp.MustCompile(`(\$\{.+?\})`)
|
||||
@@ -21,65 +19,6 @@ func (e DashboardInputMissingError) Error() string {
|
||||
return fmt.Sprintf("Dashboard input variable: %v missing from import command", e.VariableName)
|
||||
}
|
||||
|
||||
func (pm *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json,
|
||||
overwrite bool, inputs []plugins.ImportDashboardInput, user *models.SignedInUser,
|
||||
requestHandler plugins.DataRequestHandler) (plugins.PluginDashboardInfoDTO, *models.Dashboard, error) {
|
||||
var dashboard *models.Dashboard
|
||||
if pluginID != "" {
|
||||
var err error
|
||||
if dashboard, err = pm.LoadPluginDashboard(pluginID, path); err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
} else {
|
||||
dashboard = models.NewDashboardFromJson(dashboardModel)
|
||||
}
|
||||
|
||||
evaluator := &DashTemplateEvaluator{
|
||||
template: dashboard.Data,
|
||||
inputs: inputs,
|
||||
}
|
||||
|
||||
generatedDash, err := evaluator.Eval()
|
||||
if err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
|
||||
saveCmd := models.SaveDashboardCommand{
|
||||
Dashboard: generatedDash,
|
||||
OrgId: orgID,
|
||||
UserId: user.UserId,
|
||||
Overwrite: overwrite,
|
||||
PluginId: pluginID,
|
||||
FolderId: folderID,
|
||||
}
|
||||
|
||||
dto := &dashboards.SaveDashboardDTO{
|
||||
OrgId: orgID,
|
||||
Dashboard: saveCmd.GetDashboardModel(),
|
||||
Overwrite: saveCmd.Overwrite,
|
||||
User: user,
|
||||
}
|
||||
|
||||
savedDash, err := dashboards.NewService(pm.SQLStore).ImportDashboard(dto)
|
||||
if err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
|
||||
return plugins.PluginDashboardInfoDTO{
|
||||
PluginId: pluginID,
|
||||
Title: savedDash.Title,
|
||||
Path: path,
|
||||
Revision: savedDash.Data.Get("revision").MustInt64(1),
|
||||
FolderId: savedDash.FolderId,
|
||||
ImportedUri: "db/" + savedDash.Slug,
|
||||
ImportedUrl: savedDash.GetUrl(),
|
||||
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
|
||||
Imported: true,
|
||||
DashboardId: savedDash.Id,
|
||||
Slug: savedDash.Slug,
|
||||
}, savedDash, nil
|
||||
}
|
||||
|
||||
type DashTemplateEvaluator struct {
|
||||
template *simplejson.Json
|
||||
inputs []plugins.ImportDashboardInput
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -25,7 +27,7 @@ func TestDashboardImport(t *testing.T) {
|
||||
info, dash, err := pm.ImportDashboard("test-app", "dashboards/connections.json", 1, 0, nil, false,
|
||||
[]plugins.ImportDashboardInput{
|
||||
{Name: "*", Type: "datasource", Value: "graphite"},
|
||||
}, &models.SignedInUser{UserId: 1, OrgRole: models.ROLE_ADMIN}, nil)
|
||||
}, &models.SignedInUser{UserId: 1, OrgRole: models.ROLE_ADMIN})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
require.NotNil(t, dash)
|
||||
@@ -88,7 +90,7 @@ func pluginScenario(t *testing.T, desc string, fn func(*testing.T, *PluginManage
|
||||
},
|
||||
},
|
||||
}
|
||||
pm := newManager(cfg, &sqlstore.SQLStore{}, &fakeBackendPluginManager{})
|
||||
pm := newManager(cfg, nil, loader.New(nil, cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}), &sqlstore.SQLStore{})
|
||||
err := pm.init()
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
func (pm *PluginManager) GetPluginDashboards(orgID int64, pluginID string) ([]*plugins.PluginDashboardInfoDTO, error) {
|
||||
plugin := pm.GetPlugin(pluginID)
|
||||
func (m *PluginManager) GetPluginDashboards(orgID int64, pluginID string) ([]*plugins.PluginDashboardInfoDTO, error) {
|
||||
plugin := m.Plugin(pluginID)
|
||||
if plugin == nil {
|
||||
return nil, plugins.PluginNotFoundError{PluginID: pluginID}
|
||||
return nil, plugins.NotFoundError{PluginID: pluginID}
|
||||
}
|
||||
|
||||
result := make([]*plugins.PluginDashboardInfoDTO, 0)
|
||||
@@ -26,18 +27,18 @@ func (pm *PluginManager) GetPluginDashboards(orgID int64, pluginID string) ([]*p
|
||||
|
||||
existingMatches := make(map[int64]bool)
|
||||
for _, include := range plugin.Includes {
|
||||
if include.Type != plugins.PluginTypeDashboard {
|
||||
if include.Type != plugins.TypeDashboard {
|
||||
continue
|
||||
}
|
||||
|
||||
dashboard, err := pm.LoadPluginDashboard(plugin.Id, include.Path)
|
||||
dashboard, err := m.LoadPluginDashboard(plugin.ID, include.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &plugins.PluginDashboardInfoDTO{}
|
||||
res.Path = include.Path
|
||||
res.PluginId = plugin.Id
|
||||
res.PluginId = plugin.ID
|
||||
res.Title = dashboard.Title
|
||||
res.Revision = dashboard.Data.Get("revision").MustInt64(1)
|
||||
|
||||
@@ -70,10 +71,10 @@ func (pm *PluginManager) GetPluginDashboards(orgID int64, pluginID string) ([]*p
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (pm *PluginManager) LoadPluginDashboard(pluginID, path string) (*models.Dashboard, error) {
|
||||
plugin := pm.GetPlugin(pluginID)
|
||||
func (m *PluginManager) LoadPluginDashboard(pluginID, path string) (*models.Dashboard, error) {
|
||||
plugin := m.Plugin(pluginID)
|
||||
if plugin == nil {
|
||||
return nil, plugins.PluginNotFoundError{PluginID: pluginID}
|
||||
return nil, plugins.NotFoundError{PluginID: pluginID}
|
||||
}
|
||||
|
||||
dashboardFilePath := filepath.Join(plugin.PluginDir, path)
|
||||
@@ -88,7 +89,7 @@ func (pm *PluginManager) LoadPluginDashboard(pluginID, path string) (*models.Das
|
||||
|
||||
defer func() {
|
||||
if err := reader.Close(); err != nil {
|
||||
plog.Warn("Failed to close file", "path", dashboardFilePath, "err", err)
|
||||
m.log.Warn("Failed to close file", "path", dashboardFilePath, "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -99,3 +100,62 @@ func (pm *PluginManager) LoadPluginDashboard(pluginID, path string) (*models.Das
|
||||
|
||||
return models.NewDashboardFromJson(data), nil
|
||||
}
|
||||
|
||||
func (m *PluginManager) ImportDashboard(pluginID, path string, orgID, folderID int64, dashboardModel *simplejson.Json,
|
||||
overwrite bool, inputs []plugins.ImportDashboardInput, user *models.SignedInUser) (plugins.PluginDashboardInfoDTO,
|
||||
*models.Dashboard, error) {
|
||||
var dashboard *models.Dashboard
|
||||
if pluginID != "" {
|
||||
var err error
|
||||
if dashboard, err = m.LoadPluginDashboard(pluginID, path); err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
} else {
|
||||
dashboard = models.NewDashboardFromJson(dashboardModel)
|
||||
}
|
||||
|
||||
evaluator := &DashTemplateEvaluator{
|
||||
template: dashboard.Data,
|
||||
inputs: inputs,
|
||||
}
|
||||
|
||||
generatedDash, err := evaluator.Eval()
|
||||
if err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
|
||||
saveCmd := models.SaveDashboardCommand{
|
||||
Dashboard: generatedDash,
|
||||
OrgId: orgID,
|
||||
UserId: user.UserId,
|
||||
Overwrite: overwrite,
|
||||
PluginId: pluginID,
|
||||
FolderId: folderID,
|
||||
}
|
||||
|
||||
dto := &dashboards.SaveDashboardDTO{
|
||||
OrgId: orgID,
|
||||
Dashboard: saveCmd.GetDashboardModel(),
|
||||
Overwrite: saveCmd.Overwrite,
|
||||
User: user,
|
||||
}
|
||||
|
||||
savedDash, err := dashboards.NewService(m.sqlStore).ImportDashboard(dto)
|
||||
if err != nil {
|
||||
return plugins.PluginDashboardInfoDTO{}, &models.Dashboard{}, err
|
||||
}
|
||||
|
||||
return plugins.PluginDashboardInfoDTO{
|
||||
PluginId: pluginID,
|
||||
Title: savedDash.Title,
|
||||
Path: path,
|
||||
Revision: savedDash.Data.Get("revision").MustInt64(1),
|
||||
FolderId: savedDash.FolderId,
|
||||
ImportedUri: "db/" + savedDash.Slug,
|
||||
ImportedUrl: savedDash.GetUrl(),
|
||||
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
|
||||
Imported: true,
|
||||
DashboardId: savedDash.Id,
|
||||
Slug: savedDash.Slug,
|
||||
}, savedDash, nil
|
||||
}
|
||||
|
||||
@@ -4,11 +4,12 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
|
||||
"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/manager/loader"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -22,7 +23,7 @@ func TestGetPluginDashboards(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
pm := newManager(cfg, &sqlstore.SQLStore{}, &fakeBackendPluginManager{})
|
||||
pm := newManager(cfg, nil, loader.New(nil, cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}), &sqlstore.SQLStore{})
|
||||
err := pm.init()
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
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,15 @@
|
||||
package installer
|
||||
|
||||
type Logger interface {
|
||||
Successf(format string, args ...interface{})
|
||||
Failuref(format string, args ...interface{})
|
||||
|
||||
Info(args ...interface{})
|
||||
Infof(format string, args ...interface{})
|
||||
Debug(args ...interface{})
|
||||
Debugf(format string, args ...interface{})
|
||||
Warn(args ...interface{})
|
||||
Warnf(format string, args ...interface{})
|
||||
Error(args ...interface{})
|
||||
Errorf(format string, args ...interface{})
|
||||
}
|
||||
@@ -33,7 +33,7 @@ type Installer struct {
|
||||
httpClient http.Client
|
||||
httpClientNoTimeout http.Client
|
||||
grafanaVersion string
|
||||
log plugins.PluginInstallerLogger
|
||||
log Logger
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -80,7 +80,7 @@ func (e ErrVersionNotFound) Error() string {
|
||||
return fmt.Sprintf("%s v%s either does not exist or is not supported on your system (%s)", e.PluginID, e.RequestedVersion, e.SystemInfo)
|
||||
}
|
||||
|
||||
func New(skipTLSVerify bool, grafanaVersion string, logger plugins.PluginInstallerLogger) *Installer {
|
||||
func New(skipTLSVerify bool, grafanaVersion string, logger Logger) plugins.Installer {
|
||||
return &Installer{
|
||||
httpClient: makeHttpClient(skipTLSVerify, 10*time.Second),
|
||||
httpClientNoTimeout: makeHttpClient(skipTLSVerify, 0),
|
||||
@@ -410,7 +410,7 @@ func normalizeVersion(version string) string {
|
||||
return normalized
|
||||
}
|
||||
|
||||
func (i *Installer) GetUpdateInfo(pluginID, version, pluginRepoURL string) (plugins.UpdateInfo, error) {
|
||||
func (i *Installer) GetUpdateInfo(ctx context.Context, pluginID, version, pluginRepoURL string) (plugins.UpdateInfo, error) {
|
||||
plugin, err := i.getPluginMetadataFromPluginRepo(pluginID, pluginRepoURL)
|
||||
if err != nil {
|
||||
return plugins.UpdateInfo{}, err
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package finder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var logger = log.New("plugin.finder")
|
||||
|
||||
type Finder struct {
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func New(cfg *setting.Cfg) Finder {
|
||||
return Finder{cfg: cfg}
|
||||
}
|
||||
|
||||
func (f *Finder) Find(pluginDirs []string) ([]string, error) {
|
||||
var pluginJSONPaths []string
|
||||
|
||||
for _, dir := range pluginDirs {
|
||||
exists, err := fs.Exists(dir)
|
||||
if err != nil {
|
||||
log.Warn("Error occurred when checking if plugin directory exists", "dir", dir, "err", err)
|
||||
}
|
||||
if !exists {
|
||||
logger.Warn("Skipping finding plugins as directory does not exist", "dir", dir)
|
||||
continue
|
||||
}
|
||||
|
||||
paths, err := f.getPluginJSONPaths(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pluginJSONPaths = append(pluginJSONPaths, paths...)
|
||||
}
|
||||
|
||||
return pluginJSONPaths, nil
|
||||
}
|
||||
|
||||
func (f *Finder) getPluginJSONPaths(dir string) ([]string, error) {
|
||||
var pluginJSONPaths []string
|
||||
|
||||
var err error
|
||||
dir, err = filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
if err := util.Walk(dir, true, true,
|
||||
func(currentPath string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("filepath.Walk reported an error for %q: %w", currentPath, err)
|
||||
}
|
||||
|
||||
if fi.Name() == "node_modules" {
|
||||
return util.ErrWalkSkipDir
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fi.Name() != "plugin.json" {
|
||||
return nil
|
||||
}
|
||||
|
||||
pluginJSONPaths = append(pluginJSONPaths, currentPath)
|
||||
return nil
|
||||
}); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
logger.Debug("Couldn't scan directory since it doesn't exist", "pluginDir", dir, "err", err)
|
||||
return []string{}, nil
|
||||
}
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
logger.Debug("Couldn't scan directory due to lack of permissions", "pluginDir", dir, "err", err)
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
return pluginJSONPaths, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package finder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestFinder_Find(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
cfg *setting.Cfg
|
||||
pluginDirs []string
|
||||
expectedPathSuffix []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "Dir with single plugin",
|
||||
cfg: setting.NewCfg(),
|
||||
pluginDirs: []string{"../../testdata/valid-v2-signature"},
|
||||
expectedPathSuffix: []string{"/pkg/plugins/manager/testdata/valid-v2-signature/plugin/plugin.json"},
|
||||
},
|
||||
{
|
||||
name: "Dir with nested plugins",
|
||||
cfg: setting.NewCfg(),
|
||||
pluginDirs: []string{"../../testdata/duplicate-plugins"},
|
||||
expectedPathSuffix: []string{
|
||||
"/pkg/plugins/manager/testdata/duplicate-plugins/nested/nested/plugin.json",
|
||||
"/pkg/plugins/manager/testdata/duplicate-plugins/nested/plugin.json",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Dir with single plugin which has symbolic link root directory",
|
||||
cfg: setting.NewCfg(),
|
||||
pluginDirs: []string{"../../testdata/symbolic-plugin-dirs"},
|
||||
expectedPathSuffix: []string{"/pkg/plugins/manager/testdata/includes-symlinks/plugin.json"},
|
||||
},
|
||||
{
|
||||
name: "Multiple plugin dirs",
|
||||
cfg: setting.NewCfg(),
|
||||
pluginDirs: []string{"../../testdata/duplicate-plugins", "../../testdata/invalid-v1-signature"},
|
||||
expectedPathSuffix: []string{
|
||||
"/pkg/plugins/manager/testdata/duplicate-plugins/nested/nested/plugin.json",
|
||||
"/pkg/plugins/manager/testdata/duplicate-plugins/nested/plugin.json",
|
||||
"/pkg/plugins/manager/testdata/invalid-v1-signature/plugin/plugin.json"},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f := New(tc.cfg)
|
||||
pluginPaths, err := f.Find(tc.pluginDirs)
|
||||
if (err != nil) && !errors.Is(err, tc.err) {
|
||||
t.Errorf("Find() error = %v, expected error %v", err, tc.err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, len(tc.expectedPathSuffix), len(pluginPaths))
|
||||
for i := 0; i < len(tc.expectedPathSuffix); i++ {
|
||||
assert.True(t, strings.HasSuffix(pluginPaths[i], tc.expectedPathSuffix[i]))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package initializer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
|
||||
"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/plugins/backendplugin/grpcplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var logger = log.New("plugin.initializer")
|
||||
|
||||
type Initializer struct {
|
||||
cfg *setting.Cfg
|
||||
license models.Licensing
|
||||
}
|
||||
|
||||
func New(cfg *setting.Cfg, license models.Licensing) Initializer {
|
||||
return Initializer{
|
||||
cfg: cfg,
|
||||
license: license,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Initializer) Initialize(p *plugins.Plugin) error {
|
||||
if len(p.Dependencies.Plugins) == 0 {
|
||||
p.Dependencies.Plugins = []plugins.Dependency{}
|
||||
}
|
||||
|
||||
if p.Dependencies.GrafanaVersion == "" {
|
||||
p.Dependencies.GrafanaVersion = "*"
|
||||
}
|
||||
|
||||
for _, include := range p.Includes {
|
||||
if include.Role == "" {
|
||||
include.Role = models.ROLE_VIEWER
|
||||
}
|
||||
}
|
||||
|
||||
i.handleModuleDefaults(p)
|
||||
|
||||
p.Info.Logos.Small = pluginLogoURL(p.Type, p.Info.Logos.Small, p.BaseURL)
|
||||
p.Info.Logos.Large = pluginLogoURL(p.Type, p.Info.Logos.Large, p.BaseURL)
|
||||
|
||||
for i := 0; i < len(p.Info.Screenshots); i++ {
|
||||
p.Info.Screenshots[i].Path = evalRelativePluginURLPath(p.Info.Screenshots[i].Path, p.BaseURL, p.Type)
|
||||
}
|
||||
|
||||
if p.IsApp() {
|
||||
for _, child := range p.Children {
|
||||
i.setPathsBasedOnApp(p, child)
|
||||
}
|
||||
|
||||
// slugify pages
|
||||
for _, include := range p.Includes {
|
||||
if include.Slug == "" {
|
||||
include.Slug = slug.Make(include.Name)
|
||||
}
|
||||
if include.Type == "page" && include.DefaultNav {
|
||||
p.DefaultNavURL = i.cfg.AppSubURL + "/plugins/" + p.ID + "/page/" + include.Slug
|
||||
}
|
||||
if include.Type == "dashboard" && include.DefaultNav {
|
||||
p.DefaultNavURL = i.cfg.AppSubURL + "/dashboard/db/" + include.Slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluginLog := logger.New("pluginID", p.ID)
|
||||
p.SetLogger(pluginLog)
|
||||
|
||||
if p.Backend {
|
||||
var backendFactory backendplugin.PluginFactoryFunc
|
||||
if p.IsRenderer() {
|
||||
cmd := plugins.ComposeRendererStartCommand()
|
||||
backendFactory = grpcplugin.NewRendererPlugin(p.ID, filepath.Join(p.PluginDir, cmd),
|
||||
func(pluginID string, renderer pluginextensionv2.RendererPlugin, logger log.Logger) error {
|
||||
p.Renderer = renderer
|
||||
return nil
|
||||
},
|
||||
)
|
||||
} else {
|
||||
cmd := plugins.ComposePluginStartCommand(p.Executable)
|
||||
backendFactory = grpcplugin.NewBackendPlugin(p.ID, filepath.Join(p.PluginDir, cmd))
|
||||
}
|
||||
|
||||
if backendClient, err := backendFactory(p.ID, pluginLog, i.envVars(p)); err != nil {
|
||||
return err
|
||||
} else {
|
||||
p.RegisterClient(backendClient)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Initializer) InitializeWithFactory(p *plugins.Plugin, factory backendplugin.PluginFactoryFunc) error {
|
||||
err := i.Initialize(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if factory != nil {
|
||||
var err error
|
||||
|
||||
f, err := factory(p.ID, log.New("pluginID", p.ID), []string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.RegisterClient(f)
|
||||
} else {
|
||||
logger.Warn("Could not initialize core plugin process", "pluginID", p.ID)
|
||||
return fmt.Errorf("could not initialize plugin %s", p.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Initializer) handleModuleDefaults(p *plugins.Plugin) {
|
||||
if p.IsCorePlugin() {
|
||||
// Previously there was an assumption that the Core plugins directory
|
||||
// should be public/app/plugins/<plugin type>/<plugin id>
|
||||
// However this can be an issue if the Core plugins directory is renamed
|
||||
baseDir := filepath.Base(p.PluginDir)
|
||||
|
||||
// use path package for the following statements because these are not file paths
|
||||
p.Module = path.Join("app/plugins", string(p.Type), baseDir, "module")
|
||||
p.BaseURL = path.Join("public/app/plugins", string(p.Type), baseDir)
|
||||
return
|
||||
}
|
||||
|
||||
metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature))
|
||||
|
||||
p.Module = path.Join("plugins", p.ID, "module")
|
||||
p.BaseURL = path.Join("public/plugins", p.ID)
|
||||
}
|
||||
|
||||
func (i *Initializer) setPathsBasedOnApp(parent *plugins.Plugin, child *plugins.Plugin) {
|
||||
appSubPath := strings.ReplaceAll(strings.Replace(child.PluginDir, parent.PluginDir, "", 1), "\\", "/")
|
||||
child.IncludedInAppID = parent.ID
|
||||
child.BaseURL = parent.BaseURL
|
||||
|
||||
if parent.IsCorePlugin() {
|
||||
child.Module = util.JoinURLFragments("app/plugins/app/"+parent.ID, appSubPath) + "/module"
|
||||
} else {
|
||||
child.Module = util.JoinURLFragments("plugins/"+parent.ID, appSubPath) + "/module"
|
||||
}
|
||||
}
|
||||
|
||||
func pluginLogoURL(pluginType plugins.Type, path, baseURL string) string {
|
||||
if path == "" {
|
||||
return defaultLogoPath(pluginType)
|
||||
}
|
||||
|
||||
return evalRelativePluginURLPath(path, baseURL, pluginType)
|
||||
}
|
||||
|
||||
func defaultLogoPath(pluginType plugins.Type) string {
|
||||
return "public/img/icn-" + string(pluginType) + ".svg"
|
||||
}
|
||||
|
||||
func evalRelativePluginURLPath(pathStr, baseURL string, pluginType plugins.Type) string {
|
||||
if pathStr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
u, _ := url.Parse(pathStr)
|
||||
if u.IsAbs() {
|
||||
return pathStr
|
||||
}
|
||||
|
||||
// is set as default or has already been prefixed with base path
|
||||
if pathStr == defaultLogoPath(pluginType) || strings.HasPrefix(pathStr, baseURL) {
|
||||
return pathStr
|
||||
}
|
||||
|
||||
return path.Join(baseURL, pathStr)
|
||||
}
|
||||
|
||||
func (i *Initializer) envVars(plugin *plugins.Plugin) []string {
|
||||
hostEnv := []string{
|
||||
fmt.Sprintf("GF_VERSION=%s", i.cfg.BuildVersion),
|
||||
}
|
||||
|
||||
if i.license != nil && i.license.HasLicense() {
|
||||
hostEnv = append(
|
||||
hostEnv,
|
||||
fmt.Sprintf("GF_EDITION=%s", i.license.Edition()),
|
||||
fmt.Sprintf("GF_ENTERPRISE_license_PATH=%s", i.cfg.EnterpriseLicensePath),
|
||||
)
|
||||
|
||||
if envProvider, ok := i.license.(models.LicenseEnvironment); ok {
|
||||
for k, v := range envProvider.Environment() {
|
||||
hostEnv = append(hostEnv, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hostEnv = append(hostEnv, i.awsEnvVars()...)
|
||||
hostEnv = append(hostEnv, i.azureEnvVars()...)
|
||||
return getPluginSettings(plugin.ID, i.cfg).asEnvVar("GF_PLUGIN", hostEnv)
|
||||
}
|
||||
|
||||
func (i *Initializer) awsEnvVars() []string {
|
||||
var variables []string
|
||||
if i.cfg.AWSAssumeRoleEnabled {
|
||||
variables = append(variables, awsds.AssumeRoleEnabledEnvVarKeyName+"=true")
|
||||
}
|
||||
if len(i.cfg.AWSAllowedAuthProviders) > 0 {
|
||||
variables = append(variables, awsds.AllowedAuthProvidersEnvVarKeyName+"="+strings.Join(i.cfg.AWSAllowedAuthProviders, ","))
|
||||
}
|
||||
|
||||
return variables
|
||||
}
|
||||
|
||||
func (i *Initializer) azureEnvVars() []string {
|
||||
var variables []string
|
||||
if i.cfg.Azure.Cloud != "" {
|
||||
variables = append(variables, "AZURE_CLOUD="+i.cfg.Azure.Cloud)
|
||||
}
|
||||
if i.cfg.Azure.ManagedIdentityClientId != "" {
|
||||
variables = append(variables, "AZURE_MANAGED_IDENTITY_CLIENT_ID="+i.cfg.Azure.ManagedIdentityClientId)
|
||||
}
|
||||
if i.cfg.Azure.ManagedIdentityEnabled {
|
||||
variables = append(variables, "AZURE_MANAGED_IDENTITY_ENABLED=true")
|
||||
}
|
||||
|
||||
return variables
|
||||
}
|
||||
|
||||
type pluginSettings map[string]string
|
||||
|
||||
func (ps pluginSettings) asEnvVar(prefix string, hostEnv []string) []string {
|
||||
var env []string
|
||||
for k, v := range ps {
|
||||
key := fmt.Sprintf("%s_%s", prefix, strings.ToUpper(k))
|
||||
if value := os.Getenv(key); value != "" {
|
||||
v = value
|
||||
}
|
||||
|
||||
env = append(env, fmt.Sprintf("%s=%s", key, v))
|
||||
}
|
||||
|
||||
env = append(env, hostEnv...)
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
func getPluginSettings(pluginID string, cfg *setting.Cfg) pluginSettings {
|
||||
ps := pluginSettings{}
|
||||
for k, v := range cfg.PluginSettings[pluginID] {
|
||||
if k == "path" || strings.ToLower(k) == "id" {
|
||||
continue
|
||||
}
|
||||
ps[k] = v
|
||||
}
|
||||
|
||||
return ps
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package initializer
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"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/setting"
|
||||
)
|
||||
|
||||
func TestInitializer_Initialize(t *testing.T) {
|
||||
absCurPath, err := filepath.Abs(".")
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Run("core backend datasource", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: plugins.DataSource,
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Name: "Example dashboard",
|
||||
Type: plugins.TypeDashboard,
|
||||
},
|
||||
},
|
||||
Backend: true,
|
||||
},
|
||||
PluginDir: absCurPath,
|
||||
Class: plugins.Core,
|
||||
}
|
||||
|
||||
i := &Initializer{
|
||||
cfg: setting.NewCfg(),
|
||||
}
|
||||
|
||||
err := i.Initialize(p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "public/img/icn-datasource.svg", p.Info.Logos.Small)
|
||||
assert.Equal(t, "public/img/icn-datasource.svg", p.Info.Logos.Large)
|
||||
assert.Equal(t, "*", p.Dependencies.GrafanaVersion)
|
||||
assert.Len(t, p.Includes, 1)
|
||||
assert.Equal(t, models.ROLE_VIEWER, p.Includes[0].Role)
|
||||
assert.Equal(t, filepath.Join("app/plugins/datasource", filepath.Base(p.PluginDir), "module"), p.Module)
|
||||
assert.Equal(t, path.Join("public/app/plugins/datasource", filepath.Base(p.PluginDir)), p.BaseURL)
|
||||
assert.NotNil(t, p.Logger())
|
||||
c, exists := p.Client()
|
||||
assert.True(t, exists)
|
||||
assert.NotNil(t, c)
|
||||
})
|
||||
|
||||
t.Run("renderer", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: plugins.Renderer,
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: ">=8.x",
|
||||
},
|
||||
Backend: true,
|
||||
},
|
||||
PluginDir: absCurPath,
|
||||
Class: plugins.External,
|
||||
}
|
||||
|
||||
i := &Initializer{
|
||||
cfg: setting.NewCfg(),
|
||||
}
|
||||
|
||||
err := i.Initialize(p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// TODO add default img to project
|
||||
assert.Equal(t, "public/img/icn-renderer.svg", p.Info.Logos.Small)
|
||||
assert.Equal(t, "public/img/icn-renderer.svg", p.Info.Logos.Large)
|
||||
assert.Equal(t, ">=8.x", p.Dependencies.GrafanaVersion)
|
||||
assert.Equal(t, "plugins/test/module", p.Module)
|
||||
assert.Equal(t, "public/plugins/test", p.BaseURL)
|
||||
assert.NotNil(t, p.Logger())
|
||||
c, exists := p.Client()
|
||||
assert.True(t, exists)
|
||||
assert.NotNil(t, c)
|
||||
})
|
||||
|
||||
t.Run("external app", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "parent-plugin",
|
||||
Type: plugins.App,
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Type: "page",
|
||||
DefaultNav: true,
|
||||
Slug: "myCustomSlug",
|
||||
},
|
||||
},
|
||||
},
|
||||
PluginDir: absCurPath,
|
||||
Class: plugins.External,
|
||||
Children: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "child-plugin",
|
||||
},
|
||||
PluginDir: absCurPath,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
i := &Initializer{
|
||||
cfg: &setting.Cfg{
|
||||
AppSubURL: "appSubURL",
|
||||
},
|
||||
}
|
||||
|
||||
err := i.Initialize(p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "public/img/icn-app.svg", p.Info.Logos.Small)
|
||||
assert.Equal(t, "public/img/icn-app.svg", p.Info.Logos.Large)
|
||||
assert.Equal(t, "*", p.Dependencies.GrafanaVersion)
|
||||
assert.Len(t, p.Includes, 1)
|
||||
assert.Equal(t, models.ROLE_VIEWER, p.Includes[0].Role)
|
||||
assert.Equal(t, filepath.Join("plugins", p.ID, "module"), p.Module)
|
||||
assert.Equal(t, "public/plugins/parent-plugin", p.BaseURL)
|
||||
assert.NotNil(t, p.Logger())
|
||||
c, exists := p.Client()
|
||||
assert.False(t, exists)
|
||||
assert.Nil(t, c)
|
||||
|
||||
assert.Len(t, p.Children, 1)
|
||||
assert.Equal(t, p.ID, p.Children[0].IncludedInAppID)
|
||||
assert.Equal(t, "public/plugins/parent-plugin", p.Children[0].BaseURL)
|
||||
assert.Equal(t, "plugins/parent-plugin/module", p.Children[0].Module)
|
||||
assert.Equal(t, "appSubURL/plugins/parent-plugin/page/myCustomSlug", p.DefaultNavURL)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInitializer_InitializeWithFactory(t *testing.T) {
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-plugin",
|
||||
Type: plugins.App,
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Type: "page",
|
||||
DefaultNav: true,
|
||||
Slug: "myCustomSlug",
|
||||
},
|
||||
},
|
||||
},
|
||||
PluginDir: "test/folder",
|
||||
Class: plugins.External,
|
||||
}
|
||||
i := &Initializer{
|
||||
cfg: &setting.Cfg{
|
||||
AppSubURL: "appSubURL",
|
||||
},
|
||||
}
|
||||
|
||||
factoryInvoked := false
|
||||
|
||||
factory := backendplugin.PluginFactoryFunc(func(pluginID string, logger log.Logger, env []string) (backendplugin.Plugin, error) {
|
||||
factoryInvoked = true
|
||||
return testPlugin{}, nil
|
||||
})
|
||||
|
||||
err := i.InitializeWithFactory(p, factory)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.True(t, factoryInvoked)
|
||||
assert.NotNil(t, p.Logger())
|
||||
client, exists := p.Client()
|
||||
assert.True(t, exists)
|
||||
assert.NotNil(t, client.(testPlugin))
|
||||
})
|
||||
|
||||
t.Run("invalid factory", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-plugin",
|
||||
Type: plugins.App,
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Type: "page",
|
||||
DefaultNav: true,
|
||||
Slug: "myCustomSlug",
|
||||
},
|
||||
},
|
||||
},
|
||||
PluginDir: "test/folder",
|
||||
Class: plugins.External,
|
||||
}
|
||||
i := &Initializer{
|
||||
cfg: &setting.Cfg{
|
||||
AppSubURL: "appSubURL",
|
||||
},
|
||||
}
|
||||
|
||||
err := i.InitializeWithFactory(p, nil)
|
||||
assert.Errorf(t, err, "could not initialize plugin test-plugin")
|
||||
|
||||
c, exists := p.Client()
|
||||
assert.False(t, exists)
|
||||
assert.Nil(t, c)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInitializer_envVars(t *testing.T) {
|
||||
t.Run("backend datasource with license", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
},
|
||||
}
|
||||
|
||||
licensing := &testLicensingService{
|
||||
edition: "test",
|
||||
hasLicense: true,
|
||||
}
|
||||
|
||||
i := &Initializer{
|
||||
cfg: &setting.Cfg{
|
||||
EnterpriseLicensePath: "/path/to/ent/license",
|
||||
PluginSettings: map[string]map[string]string{
|
||||
"test": {
|
||||
"custom_env_var": "customVal",
|
||||
},
|
||||
},
|
||||
},
|
||||
license: licensing,
|
||||
}
|
||||
|
||||
envVars := i.envVars(p)
|
||||
assert.Len(t, envVars, 5)
|
||||
assert.Equal(t, "GF_PLUGIN_CUSTOM_ENV_VAR=customVal", envVars[0])
|
||||
assert.Equal(t, "GF_VERSION=", envVars[1])
|
||||
assert.Equal(t, "GF_EDITION=test", envVars[2])
|
||||
assert.Equal(t, "GF_ENTERPRISE_license_PATH=/path/to/ent/license", envVars[3])
|
||||
assert.Equal(t, "GF_ENTERPRISE_LICENSE_TEXT=", envVars[4])
|
||||
})
|
||||
}
|
||||
|
||||
func TestInitializer_setPathsBasedOnApp(t *testing.T) {
|
||||
t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) {
|
||||
i := &Initializer{
|
||||
cfg: setting.NewCfg(),
|
||||
}
|
||||
|
||||
child := &plugins.Plugin{
|
||||
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata\\datasources\\datasource",
|
||||
}
|
||||
parent := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "testdata",
|
||||
},
|
||||
Class: plugins.Core,
|
||||
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata",
|
||||
BaseURL: "public/app/plugins/app/testdata",
|
||||
}
|
||||
|
||||
i.setPathsBasedOnApp(parent, child)
|
||||
|
||||
assert.Equal(t, "app/plugins/app/testdata/datasources/datasource/module", child.Module)
|
||||
assert.Equal(t, "testdata", child.IncludedInAppID)
|
||||
assert.Equal(t, "public/app/plugins/app/testdata", child.BaseURL)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInitializer_getAWSEnvironmentVariables(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestInitializer_getAzureEnvironmentVariables(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestInitializer_handleModuleDefaults(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func Test_defaultLogoPath(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func Test_evalRelativePluginUrlPath(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func Test_getPluginLogoUrl(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func Test_getPluginSettings(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func Test_pluginSettings_ToEnv(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
type testLicensingService struct {
|
||||
edition string
|
||||
hasLicense bool
|
||||
tokenRaw string
|
||||
}
|
||||
|
||||
func (t *testLicensingService) HasLicense() bool {
|
||||
return t.hasLicense
|
||||
}
|
||||
|
||||
func (t *testLicensingService) Expiry() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (t *testLicensingService) Edition() string {
|
||||
return t.edition
|
||||
}
|
||||
|
||||
func (t *testLicensingService) StateInfo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *testLicensingService) ContentDeliveryPrefix() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *testLicensingService) LicenseURL(showAdminLicensingPage bool) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *testLicensingService) HasValidLicense() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *testLicensingService) Environment() map[string]string {
|
||||
return map[string]string{"GF_ENTERPRISE_LICENSE_TEXT": t.tokenRaw}
|
||||
}
|
||||
|
||||
type testPlugin struct {
|
||||
backendplugin.Plugin
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
"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/manager/loader/finder"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = log.New("plugin.loader")
|
||||
ErrInvalidPluginJSON = errors.New("did not find valid type or id properties in plugin.json")
|
||||
ErrInvalidPluginJSONFilePath = errors.New("invalid plugin.json filepath was provided")
|
||||
)
|
||||
|
||||
var _ plugins.ErrorResolver = (*Loader)(nil)
|
||||
|
||||
type Loader struct {
|
||||
cfg *setting.Cfg
|
||||
pluginFinder finder.Finder
|
||||
pluginInitializer initializer.Initializer
|
||||
signatureValidator signature.Validator
|
||||
|
||||
errs map[string]*plugins.SignatureError
|
||||
}
|
||||
|
||||
func ProvideService(license models.Licensing, cfg *setting.Cfg, authorizer plugins.PluginLoaderAuthorizer) (*Loader, error) {
|
||||
return New(license, cfg, authorizer), nil
|
||||
}
|
||||
|
||||
func New(license models.Licensing, cfg *setting.Cfg, authorizer plugins.PluginLoaderAuthorizer) *Loader {
|
||||
return &Loader{
|
||||
cfg: cfg,
|
||||
pluginFinder: finder.New(cfg),
|
||||
pluginInitializer: initializer.New(cfg, license),
|
||||
signatureValidator: signature.NewValidator(cfg, authorizer),
|
||||
errs: make(map[string]*plugins.SignatureError),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Loader) Load(paths []string, ignore map[string]struct{}) ([]*plugins.Plugin, error) {
|
||||
pluginJSONPaths, err := l.pluginFinder.Find(paths)
|
||||
if err != nil {
|
||||
logger.Error("plugin finder encountered an error", "err", err)
|
||||
}
|
||||
|
||||
return l.loadPlugins(pluginJSONPaths, ignore)
|
||||
}
|
||||
|
||||
func (l *Loader) LoadWithFactory(path string, factory backendplugin.PluginFactoryFunc) (*plugins.Plugin, error) {
|
||||
p, err := l.load(path, map[string]struct{}{})
|
||||
if err != nil {
|
||||
logger.Error("failed to load core plugin", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = l.pluginInitializer.InitializeWithFactory(p, factory)
|
||||
|
||||
return p, err
|
||||
}
|
||||
|
||||
func (l *Loader) load(path string, ignore map[string]struct{}) (*plugins.Plugin, error) {
|
||||
pluginJSONPaths, err := l.pluginFinder.Find([]string{path})
|
||||
if err != nil {
|
||||
logger.Error("failed to find plugin", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loadedPlugins, err := l.loadPlugins(pluginJSONPaths, ignore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(loadedPlugins) == 0 {
|
||||
return nil, fmt.Errorf("could not load plugin at path %s", path)
|
||||
}
|
||||
|
||||
return loadedPlugins[0], nil
|
||||
}
|
||||
|
||||
func (l *Loader) loadPlugins(pluginJSONPaths []string, existingPlugins map[string]struct{}) ([]*plugins.Plugin, error) {
|
||||
var foundPlugins = foundPlugins{}
|
||||
|
||||
// load plugin.json files and map directory to JSON data
|
||||
for _, pluginJSONPath := range pluginJSONPaths {
|
||||
plugin, err := l.readPluginJSON(pluginJSONPath)
|
||||
if err != nil {
|
||||
logger.Warn("Skipping plugin loading as it's plugin.json is invalid", "id", plugin.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
pluginJSONAbsPath, err := filepath.Abs(pluginJSONPath)
|
||||
if err != nil {
|
||||
logger.Warn("Skipping plugin loading as full plugin.json path could not be calculated", "id", plugin.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, dupe := foundPlugins[filepath.Dir(pluginJSONAbsPath)]; dupe {
|
||||
logger.Warn("Skipping plugin loading as it's a duplicate", "id", plugin.ID)
|
||||
continue
|
||||
}
|
||||
foundPlugins[filepath.Dir(pluginJSONAbsPath)] = plugin
|
||||
}
|
||||
|
||||
foundPlugins.stripDuplicates(existingPlugins)
|
||||
|
||||
// calculate initial signature state
|
||||
loadedPlugins := make(map[string]*plugins.Plugin)
|
||||
for pluginDir, pluginJSON := range foundPlugins {
|
||||
plugin := &plugins.Plugin{
|
||||
JSONData: pluginJSON,
|
||||
PluginDir: pluginDir,
|
||||
Class: l.pluginClass(pluginDir),
|
||||
}
|
||||
|
||||
sig, err := signature.Calculate(logger, plugin)
|
||||
if err != nil {
|
||||
logger.Warn("Could not calculate plugin signature state", "pluginID", plugin.ID, "err", err)
|
||||
continue
|
||||
}
|
||||
plugin.Signature = sig.Status
|
||||
plugin.SignatureType = sig.Type
|
||||
plugin.SignatureOrg = sig.SigningOrg
|
||||
plugin.SignedFiles = sig.Files
|
||||
|
||||
loadedPlugins[plugin.PluginDir] = plugin
|
||||
}
|
||||
|
||||
// wire up plugin dependencies
|
||||
for _, plugin := range loadedPlugins {
|
||||
ancestors := strings.Split(plugin.PluginDir, string(filepath.Separator))
|
||||
ancestors = ancestors[0 : len(ancestors)-1]
|
||||
pluginPath := ""
|
||||
|
||||
if runtime.GOOS != "windows" && filepath.IsAbs(plugin.PluginDir) {
|
||||
pluginPath = "/"
|
||||
}
|
||||
for _, ancestor := range ancestors {
|
||||
pluginPath = filepath.Join(pluginPath, ancestor)
|
||||
if parentPlugin, ok := loadedPlugins[pluginPath]; ok {
|
||||
plugin.Parent = parentPlugin
|
||||
plugin.Parent.Children = append(plugin.Parent.Children, plugin)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validate signatures
|
||||
verifiedPlugins := []*plugins.Plugin{}
|
||||
for _, plugin := range loadedPlugins {
|
||||
signingError := l.signatureValidator.Validate(plugin)
|
||||
if signingError != nil {
|
||||
logger.Warn("Skipping loading plugin due to problem with signature",
|
||||
"pluginID", plugin.ID, "status", signingError.SignatureStatus)
|
||||
plugin.SignatureError = signingError
|
||||
l.errs[plugin.ID] = signingError
|
||||
// skip plugin so it will not be loaded any further
|
||||
continue
|
||||
}
|
||||
|
||||
// clear plugin error if a pre-existing error has since been resolved
|
||||
delete(l.errs, plugin.ID)
|
||||
|
||||
// verify module.js exists for SystemJS to load
|
||||
if !plugin.IsRenderer() && !plugin.IsCorePlugin() {
|
||||
module := filepath.Join(plugin.PluginDir, "module.js")
|
||||
if exists, err := fs.Exists(module); err != nil {
|
||||
return nil, err
|
||||
} else if !exists {
|
||||
logger.Warn("Plugin missing module.js",
|
||||
"pluginID", plugin.ID,
|
||||
"warning", "Missing module.js, If you loaded this plugin from git, make sure to compile it.",
|
||||
"path", module)
|
||||
}
|
||||
}
|
||||
|
||||
verifiedPlugins = append(verifiedPlugins, plugin)
|
||||
}
|
||||
|
||||
for _, p := range verifiedPlugins {
|
||||
err := l.pluginInitializer.Initialize(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return verifiedPlugins, nil
|
||||
}
|
||||
|
||||
func (l *Loader) readPluginJSON(pluginJSONPath string) (plugins.JSONData, error) {
|
||||
logger.Debug("Loading plugin", "path", pluginJSONPath)
|
||||
|
||||
if !strings.EqualFold(filepath.Ext(pluginJSONPath), ".json") {
|
||||
return plugins.JSONData{}, ErrInvalidPluginJSONFilePath
|
||||
}
|
||||
|
||||
// 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(pluginJSONPath)
|
||||
if err != nil {
|
||||
return plugins.JSONData{}, err
|
||||
}
|
||||
|
||||
plugin := plugins.JSONData{}
|
||||
if err := json.NewDecoder(reader).Decode(&plugin); err != nil {
|
||||
return plugins.JSONData{}, err
|
||||
}
|
||||
|
||||
if err := reader.Close(); err != nil {
|
||||
logger.Warn("Failed to close JSON file", "path", pluginJSONPath, "err", err)
|
||||
}
|
||||
|
||||
if err := validatePluginJSON(plugin); err != nil {
|
||||
return plugins.JSONData{}, err
|
||||
}
|
||||
|
||||
if plugin.ID == "grafana-piechart-panel" {
|
||||
plugin.Name = "Pie Chart (old)"
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
func (l *Loader) PluginErrors() []*plugins.Error {
|
||||
errs := make([]*plugins.Error, 0)
|
||||
for _, err := range l.errs {
|
||||
errs = append(errs, &plugins.Error{
|
||||
PluginID: err.PluginID,
|
||||
ErrorCode: err.AsErrorCode(),
|
||||
})
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func validatePluginJSON(data plugins.JSONData) error {
|
||||
if data.ID == "" || !data.Type.IsValid() {
|
||||
return ErrInvalidPluginJSON
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Loader) pluginClass(pluginDir string) plugins.Class {
|
||||
isSubDir := func(base, target string) bool {
|
||||
path, err := filepath.Rel(base, target)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(path, "..") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
corePluginsDir := filepath.Join(l.cfg.StaticRootPath, "app/plugins")
|
||||
if isSubDir(corePluginsDir, pluginDir) {
|
||||
return plugins.Core
|
||||
}
|
||||
|
||||
if isSubDir(l.cfg.BundledPluginsPath, pluginDir) {
|
||||
return plugins.Bundled
|
||||
}
|
||||
|
||||
return plugins.External
|
||||
}
|
||||
|
||||
type foundPlugins map[string]plugins.JSONData
|
||||
|
||||
// stripDuplicates will strip duplicate plugins or plugins that already exist
|
||||
func (f *foundPlugins) stripDuplicates(existingPlugins map[string]struct{}) {
|
||||
pluginsByID := make(map[string]struct{})
|
||||
for path, scannedPlugin := range *f {
|
||||
if _, existing := existingPlugins[scannedPlugin.ID]; existing {
|
||||
logger.Debug("Skipping plugin as it's already installed", "plugin", scannedPlugin.ID)
|
||||
delete(*f, path)
|
||||
continue
|
||||
}
|
||||
|
||||
pluginsByID[scannedPlugin.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,928 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/finder"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader/initializer"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var compareOpts = cmpopts.IgnoreFields(plugins.Plugin{}, "client", "log")
|
||||
|
||||
func TestLoader_Load(t *testing.T) {
|
||||
corePluginDir, err := filepath.Abs("./../../../../public")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of core plugins dir")
|
||||
return
|
||||
}
|
||||
parentDir, err := filepath.Abs("../")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of current dir")
|
||||
return
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *setting.Cfg
|
||||
pluginPaths []string
|
||||
existingPlugins map[string]struct{}
|
||||
want []*plugins.Plugin
|
||||
pluginErrors map[string]*plugins.Error
|
||||
}{
|
||||
{
|
||||
name: "Load a Core plugin",
|
||||
cfg: &setting.Cfg{
|
||||
StaticRootPath: corePluginDir,
|
||||
},
|
||||
pluginPaths: []string{filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch")},
|
||||
want: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "cloudwatch",
|
||||
Type: "datasource",
|
||||
Name: "CloudWatch",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Grafana Labs",
|
||||
URL: "https://grafana.com",
|
||||
},
|
||||
Description: "Data source for Amazon AWS monitoring service",
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
|
||||
Large: "public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png",
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{Name: "EC2", Path: "dashboards/ec2.json", Type: "dashboard", Role: "Viewer"},
|
||||
{Name: "EBS", Path: "dashboards/EBS.json", Type: "dashboard", Role: "Viewer"},
|
||||
{Name: "Lambda", Path: "dashboards/Lambda.json", Type: "dashboard", Role: "Viewer"},
|
||||
{Name: "Logs", Path: "dashboards/Logs.json", Type: "dashboard", Role: "Viewer"},
|
||||
{Name: "RDS", Path: "dashboards/RDS.json", Type: "dashboard", Role: "Viewer"},
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
Category: "cloud",
|
||||
Annotations: true,
|
||||
Metrics: true,
|
||||
Alerting: true,
|
||||
Logs: true,
|
||||
QueryOptions: map[string]bool{"minInterval": true},
|
||||
},
|
||||
Module: "app/plugins/datasource/cloudwatch/module",
|
||||
BaseURL: "public/app/plugins/datasource/cloudwatch",
|
||||
PluginDir: filepath.Join(corePluginDir, "app/plugins/datasource/cloudwatch"),
|
||||
Signature: "internal",
|
||||
Class: "core",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Load a Bundled plugin",
|
||||
cfg: &setting.Cfg{
|
||||
BundledPluginsPath: filepath.Join(parentDir, "testdata"),
|
||||
},
|
||||
pluginPaths: []string{"../testdata/valid-v2-signature"},
|
||||
want: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: "datasource",
|
||||
Name: "Test",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Will Browne",
|
||||
URL: "https://willbrowne.com",
|
||||
},
|
||||
Version: "1.0.0",
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-datasource.svg",
|
||||
Large: "public/img/icn-datasource.svg",
|
||||
},
|
||||
Description: "Test",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
Executable: "test",
|
||||
Backend: true,
|
||||
State: "alpha",
|
||||
},
|
||||
Module: "plugins/test/module",
|
||||
BaseURL: "public/plugins/test",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/valid-v2-signature/plugin/"),
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
Class: "bundled",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "Load an External plugin",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
},
|
||||
pluginPaths: []string{"../testdata/symbolic-plugin-dirs"},
|
||||
want: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-app",
|
||||
Type: "app",
|
||||
Name: "Test App",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Test Inc.",
|
||||
URL: "http://test.com",
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/plugins/test-app/img/logo_small.png",
|
||||
Large: "public/plugins/test-app/img/logo_large.png",
|
||||
},
|
||||
Links: []plugins.InfoLink{
|
||||
{Name: "Project site", URL: "http://project.com"},
|
||||
{Name: "License & Terms", URL: "http://license.com"},
|
||||
},
|
||||
Description: "Official Grafana Test App & Dashboard bundle",
|
||||
Screenshots: []plugins.Screenshots{
|
||||
{Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"},
|
||||
{Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"},
|
||||
},
|
||||
Version: "1.0.0",
|
||||
Updated: "2015-02-10",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "3.x.x",
|
||||
Plugins: []plugins.Dependency{
|
||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{
|
||||
Name: "Nginx Connections",
|
||||
Path: "dashboards/connections.json",
|
||||
Type: "dashboard",
|
||||
Role: "Viewer",
|
||||
Slug: "nginx-connections",
|
||||
},
|
||||
{
|
||||
Name: "Nginx Memory",
|
||||
Path: "dashboards/memory.json",
|
||||
Type: "dashboard",
|
||||
Role: "Viewer",
|
||||
Slug: "nginx-memory",
|
||||
},
|
||||
{
|
||||
Name: "Nginx Panel",
|
||||
Type: "panel",
|
||||
Role: "Viewer",
|
||||
Slug: "nginx-panel"},
|
||||
{
|
||||
Name: "Nginx Datasource",
|
||||
Type: "datasource",
|
||||
Role: "Viewer",
|
||||
Slug: "nginx-datasource",
|
||||
},
|
||||
},
|
||||
},
|
||||
Class: plugins.External,
|
||||
Module: "plugins/test-app/module",
|
||||
BaseURL: "public/plugins/test-app",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/includes-symlinks"),
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "Load an unsigned plugin (development)",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
Env: "development",
|
||||
},
|
||||
pluginPaths: []string{"../testdata/unsigned-datasource"},
|
||||
want: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: "datasource",
|
||||
Name: "Test",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Grafana Labs",
|
||||
URL: "https://grafana.com",
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-datasource.svg",
|
||||
Large: "public/img/icn-datasource.svg",
|
||||
},
|
||||
Description: "Test",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
Backend: true,
|
||||
State: plugins.AlphaRelease,
|
||||
},
|
||||
Class: plugins.External,
|
||||
Module: "plugins/test/module",
|
||||
BaseURL: "public/plugins/test",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/unsigned-datasource/plugin"),
|
||||
Signature: "unsigned",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "Load an unsigned plugin (production)",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
Env: "production",
|
||||
},
|
||||
pluginPaths: []string{"../testdata/unsigned-datasource"},
|
||||
want: []*plugins.Plugin{},
|
||||
pluginErrors: map[string]*plugins.Error{
|
||||
"test": {
|
||||
PluginID: "test",
|
||||
ErrorCode: "signatureMissing",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Load an unsigned plugin using PluginsAllowUnsigned config (production)",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
Env: "production",
|
||||
PluginsAllowUnsigned: []string{"test"},
|
||||
},
|
||||
pluginPaths: []string{"../testdata/unsigned-datasource"},
|
||||
want: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: "datasource",
|
||||
Name: "Test",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Grafana Labs",
|
||||
URL: "https://grafana.com",
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-datasource.svg",
|
||||
Large: "public/img/icn-datasource.svg",
|
||||
},
|
||||
Description: "Test",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
Backend: true,
|
||||
State: plugins.AlphaRelease,
|
||||
},
|
||||
Class: plugins.External,
|
||||
Module: "plugins/test/module",
|
||||
BaseURL: "public/plugins/test",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/unsigned-datasource/plugin"),
|
||||
Signature: "unsigned",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Load an unsigned plugin with modified signature (production)",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
Env: "production",
|
||||
},
|
||||
pluginPaths: []string{"../testdata/lacking-files"},
|
||||
want: []*plugins.Plugin{},
|
||||
pluginErrors: map[string]*plugins.Error{
|
||||
"test": {
|
||||
PluginID: "test",
|
||||
ErrorCode: "signatureModified",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Load an unsigned plugin with modified signature using PluginsAllowUnsigned config (production) still includes a signing error",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
Env: "production",
|
||||
PluginsAllowUnsigned: []string{"test"},
|
||||
},
|
||||
pluginPaths: []string{"../testdata/lacking-files"},
|
||||
want: []*plugins.Plugin{},
|
||||
pluginErrors: map[string]*plugins.Error{
|
||||
"test": {
|
||||
PluginID: "test",
|
||||
ErrorCode: "signatureModified",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
l := newLoader(tt.cfg)
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := l.Load(tt.pluginPaths, tt.existingPlugins)
|
||||
require.NoError(t, err)
|
||||
if !cmp.Equal(got, tt.want, compareOpts) {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts))
|
||||
}
|
||||
|
||||
pluginErrs := l.PluginErrors()
|
||||
assert.Equal(t, len(tt.pluginErrors), len(pluginErrs))
|
||||
for _, pluginErr := range pluginErrs {
|
||||
assert.Equal(t, tt.pluginErrors[pluginErr.PluginID], pluginErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoader_Load_MultiplePlugins(t *testing.T) {
|
||||
parentDir, err := filepath.Abs("../")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of current dir")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("Load multiple", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *setting.Cfg
|
||||
pluginPaths []string
|
||||
appURL string
|
||||
existingPlugins map[string]struct{}
|
||||
want []*plugins.Plugin
|
||||
pluginErrors map[string]*plugins.Error
|
||||
}{
|
||||
{
|
||||
name: "Load multiple plugins (broken, valid, unsigned)",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: filepath.Join(parentDir),
|
||||
},
|
||||
appURL: "http://localhost:3000",
|
||||
pluginPaths: []string{
|
||||
"../testdata/invalid-plugin-json", // test-app
|
||||
"../testdata/valid-v2-pvt-signature", // test
|
||||
"../testdata/unsigned-panel", // test-panel
|
||||
},
|
||||
want: []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: "datasource",
|
||||
Name: "Test",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Will Browne",
|
||||
URL: "https://willbrowne.com",
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-datasource.svg",
|
||||
Large: "public/img/icn-datasource.svg",
|
||||
},
|
||||
Description: "Test",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
Backend: true,
|
||||
Executable: "test",
|
||||
State: plugins.AlphaRelease,
|
||||
},
|
||||
Class: plugins.External,
|
||||
Module: "plugins/test/module",
|
||||
BaseURL: "public/plugins/test",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/valid-v2-pvt-signature/plugin"),
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.PrivateSignature,
|
||||
SignatureOrg: "Will Browne",
|
||||
},
|
||||
},
|
||||
pluginErrors: map[string]*plugins.Error{
|
||||
"test": {
|
||||
PluginID: "test",
|
||||
ErrorCode: "signatureMissing",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
l := newLoader(tt.cfg)
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
origAppURL := setting.AppUrl
|
||||
t.Cleanup(func() {
|
||||
setting.AppUrl = origAppURL
|
||||
})
|
||||
setting.AppUrl = tt.appURL
|
||||
|
||||
got, err := l.Load(tt.pluginPaths, tt.existingPlugins)
|
||||
require.NoError(t, err)
|
||||
sort.SliceStable(got, func(i, j int) bool {
|
||||
return got[i].ID < got[j].ID
|
||||
})
|
||||
if !cmp.Equal(got, tt.want, compareOpts) {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, tt.want, compareOpts))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoader_Signature_RootURL(t *testing.T) {
|
||||
const defaultAppURL = "http://localhost:3000/grafana"
|
||||
|
||||
parentDir, err := filepath.Abs("../")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of current dir")
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("Private signature verification ignores trailing slash in root URL", func(t *testing.T) {
|
||||
origAppURL := setting.AppUrl
|
||||
origAppSubURL := setting.AppSubUrl
|
||||
t.Cleanup(func() {
|
||||
setting.AppUrl = origAppURL
|
||||
setting.AppSubUrl = origAppSubURL
|
||||
})
|
||||
setting.AppUrl = defaultAppURL
|
||||
|
||||
paths := []string{"../testdata/valid-v2-pvt-signature-root-url-uri"}
|
||||
|
||||
expected := []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: "datasource",
|
||||
Name: "Test",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{Name: "Will Browne", URL: "https://willbrowne.com"},
|
||||
Description: "Test",
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-datasource.svg",
|
||||
Large: "public/img/icn-datasource.svg",
|
||||
},
|
||||
Version: "1.0.0",
|
||||
},
|
||||
State: plugins.AlphaRelease,
|
||||
Dependencies: plugins.Dependencies{GrafanaVersion: "*", Plugins: []plugins.Dependency{}},
|
||||
Backend: true,
|
||||
Executable: "test",
|
||||
},
|
||||
PluginDir: filepath.Join(parentDir, "/testdata/valid-v2-pvt-signature-root-url-uri/plugin"),
|
||||
Class: "external",
|
||||
Signature: "valid",
|
||||
SignatureType: "private",
|
||||
SignatureOrg: "Will Browne",
|
||||
Module: "plugins/test/module",
|
||||
BaseURL: "public/plugins/test",
|
||||
},
|
||||
}
|
||||
|
||||
l := newLoader(&setting.Cfg{PluginsPath: filepath.Join(parentDir)})
|
||||
got, err := l.Load(paths, map[string]struct{}{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !cmp.Equal(got, expected, compareOpts) {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoader_Load_DuplicatePlugins(t *testing.T) {
|
||||
t.Run("Load duplicate plugin folders", func(t *testing.T) {
|
||||
pluginDir, err := filepath.Abs("../testdata/test-app")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of plugin dir")
|
||||
return
|
||||
}
|
||||
expected := []*plugins.Plugin{
|
||||
{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-app",
|
||||
Type: "app",
|
||||
Name: "Test App",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Test Inc.",
|
||||
URL: "http://test.com",
|
||||
},
|
||||
Description: "Official Grafana Test App & Dashboard bundle",
|
||||
Version: "1.0.0",
|
||||
Links: []plugins.InfoLink{
|
||||
{Name: "Project site", URL: "http://project.com"},
|
||||
{Name: "License & Terms", URL: "http://license.com"},
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/plugins/test-app/img/logo_small.png",
|
||||
Large: "public/plugins/test-app/img/logo_large.png",
|
||||
},
|
||||
Screenshots: []plugins.Screenshots{
|
||||
{Path: "public/plugins/test-app/img/screenshot1.png", Name: "img1"},
|
||||
{Path: "public/plugins/test-app/img/screenshot2.png", Name: "img2"},
|
||||
},
|
||||
Updated: "2015-02-10",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "3.x.x",
|
||||
Plugins: []plugins.Dependency{
|
||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: "Viewer", Slug: "nginx-connections"},
|
||||
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: "Viewer", Slug: "nginx-memory"},
|
||||
{Name: "Nginx Panel", Type: "panel", Role: "Viewer", Slug: "nginx-panel"},
|
||||
{Name: "Nginx Datasource", Type: "datasource", Role: "Viewer", Slug: "nginx-datasource"},
|
||||
},
|
||||
Backend: false,
|
||||
},
|
||||
PluginDir: pluginDir,
|
||||
Class: plugins.External,
|
||||
Signature: plugins.SignatureValid,
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
Module: "plugins/test-app/module",
|
||||
BaseURL: "public/plugins/test-app",
|
||||
},
|
||||
}
|
||||
|
||||
l := newLoader(&setting.Cfg{
|
||||
PluginsPath: filepath.Dir(pluginDir),
|
||||
})
|
||||
|
||||
got, err := l.Load([]string{pluginDir, pluginDir}, map[string]struct{}{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
if !cmp.Equal(got, expected, compareOpts) {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoader_loadNestedPlugins(t *testing.T) {
|
||||
parentDir, err := filepath.Abs("../")
|
||||
if err != nil {
|
||||
t.Errorf("could not construct absolute path of root dir")
|
||||
return
|
||||
}
|
||||
parent := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-ds",
|
||||
Type: "datasource",
|
||||
Name: "Parent",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Grafana Labs",
|
||||
URL: "http://grafana.com",
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-datasource.svg",
|
||||
Large: "public/img/icn-datasource.svg",
|
||||
},
|
||||
Description: "Parent plugin",
|
||||
Version: "1.0.0",
|
||||
Updated: "2020-10-20",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
Backend: true,
|
||||
},
|
||||
Module: "plugins/test-ds/module",
|
||||
BaseURL: "public/plugins/test-ds",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/nested-plugins/parent"),
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
Class: "external",
|
||||
}
|
||||
|
||||
child := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test-panel",
|
||||
Type: "panel",
|
||||
Name: "Child",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Grafana Labs",
|
||||
URL: "http://grafana.com",
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "public/img/icn-panel.svg",
|
||||
Large: "public/img/icn-panel.svg",
|
||||
},
|
||||
Description: "Child plugin",
|
||||
Version: "1.0.1",
|
||||
Updated: "2020-10-30",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "*",
|
||||
Plugins: []plugins.Dependency{},
|
||||
},
|
||||
},
|
||||
Module: "plugins/test-panel/module",
|
||||
BaseURL: "public/plugins/test-panel",
|
||||
PluginDir: filepath.Join(parentDir, "testdata/nested-plugins/parent/nested"),
|
||||
Signature: "valid",
|
||||
SignatureType: plugins.GrafanaSignature,
|
||||
SignatureOrg: "Grafana Labs",
|
||||
Class: "external",
|
||||
}
|
||||
|
||||
parent.Children = []*plugins.Plugin{child}
|
||||
child.Parent = parent
|
||||
|
||||
t.Run("Load nested External plugins", func(t *testing.T) {
|
||||
expected := []*plugins.Plugin{parent, child}
|
||||
l := newLoader(&setting.Cfg{
|
||||
PluginsPath: parentDir,
|
||||
})
|
||||
|
||||
got, err := l.Load([]string{"../testdata/nested-plugins"}, map[string]struct{}{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// to ensure we can compare with expected
|
||||
sort.SliceStable(got, func(i, j int) bool {
|
||||
return got[i].ID < got[j].ID
|
||||
})
|
||||
|
||||
if !cmp.Equal(got, expected, compareOpts) {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Load will exclude plugins that already exist", func(t *testing.T) {
|
||||
// parent/child links will not be created when either plugins are provided in the existingPlugins map
|
||||
parent.Children = nil
|
||||
expected := []*plugins.Plugin{parent}
|
||||
|
||||
l := newLoader(&setting.Cfg{
|
||||
PluginsPath: parentDir,
|
||||
})
|
||||
|
||||
got, err := l.Load([]string{"../testdata/nested-plugins"}, map[string]struct{}{
|
||||
"test-panel": {},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// to ensure we can compare with expected
|
||||
sort.SliceStable(got, func(i, j int) bool {
|
||||
return got[i].ID < got[j].ID
|
||||
})
|
||||
|
||||
if !cmp.Equal(got, expected, compareOpts) {
|
||||
t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoader_readPluginJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pluginPath string
|
||||
expected plugins.JSONData
|
||||
failed bool
|
||||
}{
|
||||
{
|
||||
name: "Valid plugin",
|
||||
pluginPath: "../testdata/test-app/plugin.json",
|
||||
expected: plugins.JSONData{
|
||||
ID: "test-app",
|
||||
Type: "app",
|
||||
Name: "Test App",
|
||||
Info: plugins.Info{
|
||||
Author: plugins.InfoLink{
|
||||
Name: "Test Inc.",
|
||||
URL: "http://test.com",
|
||||
},
|
||||
Description: "Official Grafana Test App & Dashboard bundle",
|
||||
Version: "1.0.0",
|
||||
Links: []plugins.InfoLink{
|
||||
{Name: "Project site", URL: "http://project.com"},
|
||||
{Name: "License & Terms", URL: "http://license.com"},
|
||||
},
|
||||
Logos: plugins.Logos{
|
||||
Small: "img/logo_small.png",
|
||||
Large: "img/logo_large.png",
|
||||
},
|
||||
Screenshots: []plugins.Screenshots{
|
||||
{Path: "img/screenshot1.png", Name: "img1"},
|
||||
{Path: "img/screenshot2.png", Name: "img2"},
|
||||
},
|
||||
Updated: "2015-02-10",
|
||||
},
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: "3.x.x",
|
||||
Plugins: []plugins.Dependency{
|
||||
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
|
||||
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
|
||||
},
|
||||
},
|
||||
Includes: []*plugins.Includes{
|
||||
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard"},
|
||||
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard"},
|
||||
{Name: "Nginx Panel", Type: "panel"},
|
||||
{Name: "Nginx Datasource", Type: "datasource"},
|
||||
},
|
||||
Backend: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid plugin JSON",
|
||||
pluginPath: "../testdata/invalid-plugin-json/plugin.json",
|
||||
failed: true,
|
||||
},
|
||||
{
|
||||
name: "Non-existing JSON file",
|
||||
pluginPath: "nonExistingFile.json",
|
||||
failed: true,
|
||||
},
|
||||
}
|
||||
|
||||
l := newLoader(nil)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := l.readPluginJSON(tt.pluginPath)
|
||||
if (err != nil) && !tt.failed {
|
||||
t.Errorf("readPluginJSON() error = %v, failed %v", err, tt.failed)
|
||||
return
|
||||
}
|
||||
if !cmp.Equal(got, tt.expected, compareOpts) {
|
||||
t.Errorf("Unexpected pluginJSONData: %v", cmp.Diff(got, tt.expected, compareOpts))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_validatePluginJSON(t *testing.T) {
|
||||
type args struct {
|
||||
data plugins.JSONData
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "Valid case",
|
||||
args: args{
|
||||
data: plugins.JSONData{
|
||||
ID: "grafana-plugin-id",
|
||||
Type: plugins.DataSource,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid plugin ID",
|
||||
args: args{
|
||||
data: plugins.JSONData{
|
||||
Type: plugins.Panel,
|
||||
},
|
||||
},
|
||||
err: ErrInvalidPluginJSON,
|
||||
},
|
||||
{
|
||||
name: "Invalid plugin type",
|
||||
args: args{
|
||||
data: plugins.JSONData{
|
||||
ID: "grafana-plugin-id",
|
||||
Type: "test",
|
||||
},
|
||||
},
|
||||
err: ErrInvalidPluginJSON,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := validatePluginJSON(tt.args.data); !errors.Is(err, tt.err) {
|
||||
t.Errorf("validatePluginJSON() = %v, want %v", err, tt.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_pluginClass(t *testing.T) {
|
||||
type args struct {
|
||||
pluginDir string
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected plugins.Class
|
||||
}{
|
||||
{
|
||||
name: "Core plugin class",
|
||||
args: args{
|
||||
pluginDir: "/root/app/plugins/test-app",
|
||||
cfg: &setting.Cfg{
|
||||
StaticRootPath: "/root",
|
||||
},
|
||||
},
|
||||
expected: plugins.Core,
|
||||
},
|
||||
{
|
||||
name: "Bundled plugin class",
|
||||
args: args{
|
||||
pluginDir: "/test-app",
|
||||
cfg: &setting.Cfg{
|
||||
BundledPluginsPath: "/test-app",
|
||||
},
|
||||
},
|
||||
expected: plugins.Bundled,
|
||||
},
|
||||
{
|
||||
name: "External plugin class",
|
||||
args: args{
|
||||
pluginDir: "/test-app",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: "/test-app",
|
||||
},
|
||||
},
|
||||
expected: plugins.External,
|
||||
},
|
||||
{
|
||||
name: "External plugin class",
|
||||
args: args{
|
||||
pluginDir: "/test-app",
|
||||
cfg: &setting.Cfg{
|
||||
PluginsPath: "/root",
|
||||
},
|
||||
},
|
||||
expected: plugins.External,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := newLoader(tt.args.cfg)
|
||||
got := l.pluginClass(tt.args.pluginDir)
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newLoader(cfg *setting.Cfg) *Loader {
|
||||
return &Loader{
|
||||
cfg: cfg,
|
||||
pluginFinder: finder.New(cfg),
|
||||
pluginInitializer: initializer.New(cfg, &fakeLicensingService{}),
|
||||
signatureValidator: signature.NewValidator(cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}),
|
||||
errs: make(map[string]*plugins.SignatureError),
|
||||
}
|
||||
}
|
||||
|
||||
type fakeLicensingService struct {
|
||||
edition string
|
||||
hasLicense bool
|
||||
tokenRaw string
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) HasLicense() bool {
|
||||
return t.hasLicense
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) Expiry() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) Edition() string {
|
||||
return t.edition
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) StateInfo() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) ContentDeliveryPrefix() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) LicenseURL(showAdminLicensingPage bool) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) HasValidLicense() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *fakeLicensingService) Environment() map[string]string {
|
||||
return map[string]string{"GF_ENTERPRISE_LICENSE_TEXT": t.tokenRaw}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ type InfraLogWrapper struct {
|
||||
debugMode bool
|
||||
}
|
||||
|
||||
func NewInstallerLogger(name string, debugMode bool) (l *InfraLogWrapper) {
|
||||
func newInstallerLogger(name string, debugMode bool) (l *InfraLogWrapper) {
|
||||
return &InfraLogWrapper{
|
||||
debugMode: debugMode,
|
||||
l: log.New(name),
|
||||
|
||||
+623
-630
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,165 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
func TestPluginManager_int_init(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
staticRootPath, err := filepath.Abs("../../../public/")
|
||||
require.NoError(t, err)
|
||||
|
||||
bundledPluginsPath, err := filepath.Abs("../../../plugins-bundled/internal")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &setting.Cfg{
|
||||
Raw: ini.Empty(),
|
||||
Env: setting.Prod,
|
||||
StaticRootPath: staticRootPath,
|
||||
BundledPluginsPath: bundledPluginsPath,
|
||||
PluginSettings: map[string]map[string]string{
|
||||
"plugin.datasource-id": {
|
||||
"path": "testdata/test-app",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
license := &licensing.OSSLicensingService{
|
||||
Cfg: cfg,
|
||||
}
|
||||
pm := newManager(cfg, nil, loader.New(license, cfg, &signature.UnsignedPluginAuthorizer{Cfg: cfg}), nil)
|
||||
|
||||
err = pm.init()
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyCorePluginCatalogue(t, pm)
|
||||
verifyBundledPlugins(t, pm)
|
||||
verifyPluginStaticRoutes(t, pm)
|
||||
}
|
||||
|
||||
func verifyCorePluginCatalogue(t *testing.T, pm *PluginManager) {
|
||||
t.Helper()
|
||||
|
||||
expPanels := map[string]struct{}{
|
||||
"alertGroups": {},
|
||||
"alertlist": {},
|
||||
"annolist": {},
|
||||
"barchart": {},
|
||||
"bargauge": {},
|
||||
"canvas": {},
|
||||
"dashlist": {},
|
||||
"debug": {},
|
||||
"gauge": {},
|
||||
"geomap": {},
|
||||
"gettingstarted": {},
|
||||
"graph": {},
|
||||
"heatmap": {},
|
||||
"histogram": {},
|
||||
"icon": {},
|
||||
"live": {},
|
||||
"logs": {},
|
||||
"news": {},
|
||||
"nodeGraph": {},
|
||||
"piechart": {},
|
||||
"pluginlist": {},
|
||||
"stat": {},
|
||||
"state-timeline": {},
|
||||
"status-history": {},
|
||||
"table": {},
|
||||
"table-old": {},
|
||||
"text": {},
|
||||
"timeseries": {},
|
||||
"welcome": {},
|
||||
"xychart": {},
|
||||
}
|
||||
|
||||
expDataSources := map[string]struct{}{
|
||||
"alertmanager": {},
|
||||
"dashboard": {},
|
||||
"input": {},
|
||||
"jaeger": {},
|
||||
"mixed": {},
|
||||
"zipkin": {},
|
||||
}
|
||||
|
||||
expApps := map[string]struct{}{
|
||||
"test-app": {},
|
||||
}
|
||||
|
||||
panels := pm.Plugins(plugins.Panel)
|
||||
assert.Equal(t, len(expPanels), len(panels))
|
||||
for _, p := range panels {
|
||||
require.NotNil(t, pm.Plugin(p.ID))
|
||||
assert.Contains(t, expPanels, p.ID)
|
||||
assert.Contains(t, pm.registeredPlugins(), p.ID)
|
||||
}
|
||||
|
||||
dataSources := pm.Plugins(plugins.DataSource)
|
||||
assert.Equal(t, len(expDataSources), len(dataSources))
|
||||
for _, ds := range dataSources {
|
||||
require.NotNil(t, pm.Plugin(ds.ID))
|
||||
assert.Contains(t, expDataSources, ds.ID)
|
||||
assert.Contains(t, pm.registeredPlugins(), ds.ID)
|
||||
}
|
||||
|
||||
apps := pm.Plugins(plugins.App)
|
||||
assert.Equal(t, len(expApps), len(apps))
|
||||
for _, app := range apps {
|
||||
require.NotNil(t, pm.Plugin(app.ID))
|
||||
require.Contains(t, expApps, app.ID)
|
||||
assert.Contains(t, pm.registeredPlugins(), app.ID)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(expPanels)+len(expDataSources)+len(expApps), len(pm.Plugins()))
|
||||
}
|
||||
|
||||
func verifyBundledPlugins(t *testing.T, pm *PluginManager) {
|
||||
t.Helper()
|
||||
|
||||
dsPlugins := make(map[string]struct{})
|
||||
for _, p := range pm.Plugins(plugins.DataSource) {
|
||||
dsPlugins[p.ID] = struct{}{}
|
||||
}
|
||||
|
||||
pluginRoutes := make(map[string]*plugins.StaticRoute)
|
||||
for _, r := range pm.Routes() {
|
||||
pluginRoutes[r.PluginID] = r
|
||||
}
|
||||
|
||||
assert.NotNil(t, pm.Plugin("input"))
|
||||
assert.NotNil(t, dsPlugins["input"])
|
||||
|
||||
for _, pluginID := range []string{"input"} {
|
||||
assert.Contains(t, pluginRoutes, pluginID)
|
||||
assert.True(t, strings.HasPrefix(pluginRoutes[pluginID].Directory, pm.Plugin("input").PluginDir))
|
||||
}
|
||||
}
|
||||
|
||||
func verifyPluginStaticRoutes(t *testing.T, pm *PluginManager) {
|
||||
pluginRoutes := make(map[string]*plugins.StaticRoute)
|
||||
for _, route := range pm.Routes() {
|
||||
pluginRoutes[route.PluginID] = route
|
||||
}
|
||||
|
||||
assert.Len(t, pluginRoutes, 2)
|
||||
|
||||
assert.Contains(t, pluginRoutes, "input")
|
||||
assert.Equal(t, pluginRoutes["input"].Directory, pm.Plugin("input").PluginDir)
|
||||
|
||||
assert.Contains(t, pluginRoutes, "test-app")
|
||||
assert.Equal(t, pluginRoutes["test-app"].Directory, pm.Plugin("test-app").PluginDir)
|
||||
}
|
||||
+666
-650
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
func (pm *PluginManager) GetPluginSettings(orgID int64) (map[string]*models.PluginSettingInfoDTO, error) {
|
||||
pluginSettings, err := pm.SQLStore.GetPluginSettings(orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginMap := make(map[string]*models.PluginSettingInfoDTO)
|
||||
for _, plug := range pluginSettings {
|
||||
pluginMap[plug.PluginId] = plug
|
||||
}
|
||||
|
||||
for _, pluginDef := range pm.Plugins() {
|
||||
// ignore entries that exists
|
||||
if _, ok := pluginMap[pluginDef.Id]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// default to enabled true
|
||||
opt := &models.PluginSettingInfoDTO{
|
||||
PluginId: pluginDef.Id,
|
||||
OrgId: orgID,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
// apps are disabled by default unless autoEnabled: true
|
||||
if app, exists := pm.apps[pluginDef.Id]; exists {
|
||||
opt.Enabled = app.AutoEnabled
|
||||
opt.Pinned = app.AutoEnabled
|
||||
}
|
||||
|
||||
// if it's included in app check app settings
|
||||
if pluginDef.IncludedInAppId != "" {
|
||||
// app components are by default disabled
|
||||
opt.Enabled = false
|
||||
|
||||
if appSettings, ok := pluginMap[pluginDef.IncludedInAppId]; ok {
|
||||
opt.Enabled = appSettings.Enabled
|
||||
}
|
||||
}
|
||||
|
||||
pluginMap[pluginDef.Id] = opt
|
||||
}
|
||||
|
||||
return pluginMap, nil
|
||||
}
|
||||
|
||||
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 enabledPlugins, err
|
||||
}
|
||||
|
||||
for _, app := range pm.Apps() {
|
||||
if b, ok := pluginSettingMap[app.Id]; 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 pm.dataSources {
|
||||
if _, exists := pluginSettingMap[ds.Id]; exists {
|
||||
enabledPlugins.DataSources[dsID] = ds
|
||||
}
|
||||
}
|
||||
|
||||
for _, panel := range pm.panels {
|
||||
if _, exists := pluginSettingMap[panel.Id]; exists {
|
||||
enabledPlugins.Panels = append(enabledPlugins.Panels, panel)
|
||||
}
|
||||
}
|
||||
|
||||
return enabledPlugins, nil
|
||||
}
|
||||
|
||||
// IsAppInstalled checks if an app plugin with provided plugin ID is installed.
|
||||
func (pm *PluginManager) IsAppInstalled(pluginID string) bool {
|
||||
_, exists := pm.apps[pluginID]
|
||||
return exists
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func ProvideService(cfg *setting.Cfg) (*UnsignedPluginAuthorizer, error) {
|
||||
return &UnsignedPluginAuthorizer{
|
||||
Cfg: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type UnsignedPluginAuthorizer struct {
|
||||
Cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func (u *UnsignedPluginAuthorizer) CanLoadPlugin(p *plugins.Plugin) bool {
|
||||
if p.Signature != plugins.SignatureUnsigned {
|
||||
return true
|
||||
}
|
||||
|
||||
if u.Cfg.Env == setting.Dev {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, pID := range u.Cfg.PluginsAllowUnsigned {
|
||||
if pID == p.ID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package manager
|
||||
package signature
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -15,13 +15,13 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/openpgp"
|
||||
"golang.org/x/crypto/openpgp/clearsign"
|
||||
|
||||
"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"
|
||||
|
||||
"golang.org/x/crypto/openpgp"
|
||||
"golang.org/x/crypto/openpgp/clearsign"
|
||||
)
|
||||
|
||||
// Soon we can fetch keys from:
|
||||
@@ -59,11 +59,11 @@ type pluginManifest struct {
|
||||
Files map[string]string `json:"files"`
|
||||
|
||||
// V2 supported fields
|
||||
ManifestVersion string `json:"manifestVersion"`
|
||||
SignatureType plugins.PluginSignatureType `json:"signatureType"`
|
||||
SignedByOrg string `json:"signedByOrg"`
|
||||
SignedByOrgName string `json:"signedByOrgName"`
|
||||
RootURLs []string `json:"rootUrls"`
|
||||
ManifestVersion string `json:"manifestVersion"`
|
||||
SignatureType plugins.SignatureType `json:"signatureType"`
|
||||
SignedByOrg string `json:"signedByOrg"`
|
||||
SignedByOrgName string `json:"signedByOrgName"`
|
||||
RootURLs []string `json:"rootUrls"`
|
||||
}
|
||||
|
||||
func (m *pluginManifest) isV2() bool {
|
||||
@@ -99,9 +99,13 @@ func readPluginManifest(body []byte) (*pluginManifest, error) {
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// getPluginSignatureState returns the signature state for a plugin.
|
||||
func getPluginSignatureState(log log.Logger, plugin *plugins.PluginBase) (plugins.PluginSignatureState, error) {
|
||||
log.Debug("Getting signature state of plugin", "plugin", plugin.Id, "isBackend", plugin.Backend)
|
||||
func Calculate(log log.Logger, plugin *plugins.Plugin) (plugins.Signature, error) {
|
||||
if plugin.IsCorePlugin() {
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInternal,
|
||||
}, nil
|
||||
}
|
||||
|
||||
manifestPath := filepath.Join(plugin.PluginDir, "MANIFEST.txt")
|
||||
|
||||
// nolint:gosec
|
||||
@@ -109,62 +113,55 @@ func getPluginSignatureState(log log.Logger, plugin *plugins.PluginBase) (plugin
|
||||
// on plugin the folder structure on disk and not user input.
|
||||
byteValue, err := ioutil.ReadFile(manifestPath)
|
||||
if err != nil || len(byteValue) < 10 {
|
||||
log.Debug("Plugin is unsigned", "id", plugin.Id)
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureUnsigned,
|
||||
log.Debug("Plugin is unsigned", "id", plugin.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureUnsigned,
|
||||
}, nil
|
||||
}
|
||||
|
||||
manifest, err := readPluginManifest(byteValue)
|
||||
if err != nil {
|
||||
log.Debug("Plugin signature invalid", "id", plugin.Id)
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureInvalid,
|
||||
log.Debug("Plugin signature invalid", "id", plugin.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInvalid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Make sure the versions all match
|
||||
if manifest.Plugin != plugin.Id || manifest.Version != plugin.Info.Version {
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureModified,
|
||||
if manifest.Plugin != plugin.ID || manifest.Version != plugin.Info.Version {
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureModified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate that private is running within defined root URLs
|
||||
if manifest.SignatureType == plugins.PrivateType {
|
||||
if manifest.SignatureType == plugins.PrivateSignature {
|
||||
appURL, err := url.Parse(setting.AppUrl)
|
||||
if err != nil {
|
||||
return plugins.PluginSignatureState{}, err
|
||||
return plugins.Signature{}, err
|
||||
}
|
||||
appSubURL, err := url.Parse(setting.AppSubUrl)
|
||||
if err != nil {
|
||||
return plugins.PluginSignatureState{}, err
|
||||
}
|
||||
appURLPath := path.Join(appSubURL.RequestURI(), appURL.RequestURI())
|
||||
|
||||
foundMatch := false
|
||||
for _, u := range manifest.RootURLs {
|
||||
rootURL, err := url.Parse(u)
|
||||
if err != nil {
|
||||
log.Warn("Could not parse plugin root URL", "plugin", plugin.Id, "rootUrl", rootURL)
|
||||
return plugins.PluginSignatureState{}, err
|
||||
log.Warn("Could not parse plugin root URL", "plugin", plugin.ID, "rootUrl", rootURL)
|
||||
return plugins.Signature{}, err
|
||||
}
|
||||
|
||||
if rootURL.Scheme == appURL.Scheme &&
|
||||
rootURL.Host == appURL.Host {
|
||||
foundMatch = path.Clean(rootURL.RequestURI()) == appURLPath
|
||||
|
||||
if foundMatch {
|
||||
break
|
||||
}
|
||||
rootURL.Host == appURL.Host &&
|
||||
path.Clean(rootURL.RequestURI()) == path.Clean(appURL.RequestURI()) {
|
||||
foundMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundMatch {
|
||||
log.Warn("Could not find root URL that matches running application URL", "plugin", plugin.Id,
|
||||
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,
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInvalid,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -172,24 +169,23 @@ func getPluginSignatureState(log log.Logger, plugin *plugins.PluginBase) (plugin
|
||||
manifestFiles := make(map[string]struct{}, len(manifest.Files))
|
||||
|
||||
// Verify the manifest contents
|
||||
log.Debug("Verifying contents of plugin manifest", "plugin", plugin.Id)
|
||||
for fp, hash := range manifest.Files {
|
||||
err = verifyHash(plugin.Id, filepath.Join(plugin.PluginDir, fp), hash)
|
||||
for p, hash := range manifest.Files {
|
||||
err = verifyHash(plugin.ID, filepath.Join(plugin.PluginDir, p), hash)
|
||||
if err != nil {
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureModified,
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureModified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
manifestFiles[fp] = struct{}{}
|
||||
manifestFiles[p] = struct{}{}
|
||||
}
|
||||
|
||||
if manifest.isV2() {
|
||||
pluginFiles, err := pluginFilesRequiringVerification(plugin)
|
||||
if err != nil {
|
||||
log.Warn("Could not collect plugin file information in directory", "pluginID", plugin.Id, "dir", plugin.PluginDir)
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureInvalid,
|
||||
log.Warn("Could not collect plugin file information in directory", "pluginID", plugin.ID, "dir", plugin.PluginDir)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureInvalid,
|
||||
}, err
|
||||
}
|
||||
|
||||
@@ -202,20 +198,18 @@ func getPluginSignatureState(log log.Logger, plugin *plugins.PluginBase) (plugin
|
||||
}
|
||||
|
||||
if len(unsignedFiles) > 0 {
|
||||
log.Warn("The following files were not included in the signature", "plugin", plugin.Id, "files", unsignedFiles)
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureModified,
|
||||
log.Warn("The following files were not included in the signature", "plugin", plugin.ID, "files", unsignedFiles)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureModified,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Everything OK
|
||||
log.Debug("Plugin signature valid", "id", plugin.Id)
|
||||
return plugins.PluginSignatureState{
|
||||
Status: plugins.PluginSignatureValid,
|
||||
log.Debug("Plugin signature valid", "id", plugin.ID)
|
||||
return plugins.Signature{
|
||||
Status: plugins.SignatureValid,
|
||||
Type: manifest.SignatureType,
|
||||
SigningOrg: manifest.SignedByOrgName,
|
||||
Files: manifestFiles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -247,9 +241,9 @@ func verifyHash(pluginID string, path string, hash string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// gets plugin filenames that require verification for plugin signing
|
||||
// pluginFilesRequiringVerification gets plugin filenames that require verification for plugin signing
|
||||
// returns filenames as a slice of posix style paths relative to plugin directory
|
||||
func pluginFilesRequiringVerification(plugin *plugins.PluginBase) ([]string, error) {
|
||||
func pluginFilesRequiringVerification(plugin *plugins.Plugin) ([]string, error) {
|
||||
var files []string
|
||||
err := filepath.Walk(plugin.PluginDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package manager
|
||||
package signature
|
||||
|
||||
import (
|
||||
"sort"
|
||||
@@ -105,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, plugins.PrivateType, manifest.SignatureType)
|
||||
assert.Equal(t, plugins.PrivateSignature, manifest.SignatureType)
|
||||
assert.Equal(t, "willbrowne", manifest.SignedByOrg)
|
||||
assert.Equal(t, "Will Browne", manifest.SignedByOrgName)
|
||||
assert.Equal(t, []string{"http://localhost:3000/"}, manifest.RootURLs)
|
||||
@@ -0,0 +1,81 @@
|
||||
package signature
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var logger = log.New("plugin.signature.validator")
|
||||
|
||||
type Validator struct {
|
||||
cfg *setting.Cfg
|
||||
authorizer plugins.PluginLoaderAuthorizer
|
||||
}
|
||||
|
||||
func NewValidator(cfg *setting.Cfg, authorizer plugins.PluginLoaderAuthorizer) Validator {
|
||||
return Validator{
|
||||
cfg: cfg,
|
||||
authorizer: authorizer,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Validator) Validate(plugin *plugins.Plugin) *plugins.SignatureError {
|
||||
if plugin.Signature == plugins.SignatureValid {
|
||||
logger.Debug("Plugin has valid signature", "id", plugin.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If a plugin is nested within another, create links to each other to inherit signature details
|
||||
if plugin.Parent != nil {
|
||||
if plugin.IsCorePlugin() || plugin.Signature == plugins.SignatureInternal {
|
||||
logger.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 {
|
||||
logger.Debug("Setting descendant plugin's signature to that of root", "plugin", plugin.ID,
|
||||
"root", plugin.Parent.ID, "signature", plugin.Signature, "rootSignature", plugin.Parent.Signature)
|
||||
plugin.Signature = plugin.Parent.Signature
|
||||
plugin.SignatureType = plugin.Parent.SignatureType
|
||||
plugin.SignatureOrg = plugin.Parent.SignatureOrg
|
||||
if plugin.Signature == plugins.SignatureValid {
|
||||
logger.Debug("Plugin has valid signature (inherited from root)", "id", plugin.ID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if plugin.IsCorePlugin() || plugin.IsBundledPlugin() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch plugin.Signature {
|
||||
case plugins.SignatureUnsigned:
|
||||
if authorized := s.authorizer.CanLoadPlugin(plugin); !authorized {
|
||||
logger.Debug("Plugin is unsigned", "pluginID", plugin.ID)
|
||||
return &plugins.SignatureError{
|
||||
PluginID: plugin.ID,
|
||||
SignatureStatus: plugins.SignatureUnsigned,
|
||||
}
|
||||
}
|
||||
logger.Warn("Permitting unsigned plugin. This is not recommended", "pluginID", plugin.ID, "pluginDir", plugin.PluginDir)
|
||||
return nil
|
||||
case plugins.SignatureInvalid:
|
||||
logger.Debug("Plugin has an invalid signature", "pluginID", plugin.ID)
|
||||
return &plugins.SignatureError{
|
||||
PluginID: plugin.ID,
|
||||
SignatureStatus: plugins.SignatureInvalid,
|
||||
}
|
||||
case plugins.SignatureModified:
|
||||
logger.Debug("Plugin has a modified signature", "pluginID", plugin.ID)
|
||||
return &plugins.SignatureError{
|
||||
PluginID: plugin.ID,
|
||||
SignatureStatus: plugins.SignatureModified,
|
||||
}
|
||||
default:
|
||||
logger.Debug("Plugin has an unrecognized plugin signature state", "pluginID", plugin.ID, "signature",
|
||||
plugin.Signature)
|
||||
return &plugins.SignatureError{
|
||||
PluginID: plugin.ID,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"id": "test-app",
|
||||
"type": "application"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
-----BEGIN PGP SIGNED MESSAGE-----
|
||||
Hash: SHA512
|
||||
|
||||
{
|
||||
"manifestVersion": "2.0.0",
|
||||
"signatureType": "grafana",
|
||||
"signedByOrg": "grafana",
|
||||
"signedByOrgName": "Grafana Labs",
|
||||
"plugin": "test-ds",
|
||||
"version": "1.0.0",
|
||||
"time": 1629461930434,
|
||||
"keyId": "7e4d0c6a708866e7",
|
||||
"files": {
|
||||
"plugin.json": "64e98031f30cfada473e0ad4b989ac10cd0c86844aab8c0d3fc36d8a9537a0b8",
|
||||
"nested/plugin.json": "e64abd35cd211e0e4682974ad5cdd1be7a0b7cd24951d302a16d9e2cb6cefea4"
|
||||
}
|
||||
}
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: OpenPGP.js v4.10.1
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
wqIEARMKAAYFAmEfnaoACgkQfk0ManCIZufwYgIJAZULZ72BKYehVw362aOJ
|
||||
IkUhCaIceQT6rSmWw60Ksxs8xkeCebMPfuxm6xqpvoquVmD2zIirCFUXE41M
|
||||
SQBys7/aAgkBaaVZvVPLUMYHIGNQXQ0wJ0j6JGn5Mn25GH4lH4vttaCFpQmx
|
||||
zwV8J/s7Ho612fU1ijH/nFM97I4nfxonQUEyEbA=
|
||||
=7sr3
|
||||
-----END PGP SIGNATURE-----
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Child",
|
||||
"id": "test-panel",
|
||||
"info": {
|
||||
"description": "Child plugin",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "http://grafana.com"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"updated": "2020-10-30"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "Parent",
|
||||
"id": "test-ds",
|
||||
"backend": true,
|
||||
"info": {
|
||||
"description": "Parent plugin",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "http://grafana.com"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"updated": "2020-10-20"
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
@@ -16,38 +15,20 @@ var (
|
||||
httpClient = http.Client{Timeout: 10 * time.Second}
|
||||
)
|
||||
|
||||
type grafanaNetPlugin struct {
|
||||
type gcomPlugin struct {
|
||||
Slug string `json:"slug"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type gitHubLatest struct {
|
||||
Stable string `json:"stable"`
|
||||
Testing string `json:"testing"`
|
||||
}
|
||||
|
||||
func (pm *PluginManager) getAllExternalPluginSlugs() string {
|
||||
var result []string
|
||||
for _, plug := range pm.plugins {
|
||||
if plug.IsCorePlugin {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, plug.Id)
|
||||
}
|
||||
|
||||
return strings.Join(result, ",")
|
||||
}
|
||||
|
||||
func (pm *PluginManager) checkForUpdates() {
|
||||
if !pm.Cfg.CheckForUpdates {
|
||||
func (m *PluginManager) checkForUpdates() {
|
||||
if !m.cfg.CheckForUpdates {
|
||||
return
|
||||
}
|
||||
|
||||
pm.log.Debug("Checking for updates")
|
||||
m.log.Debug("Checking for updates")
|
||||
|
||||
pluginSlugs := pm.getAllExternalPluginSlugs()
|
||||
resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
|
||||
pluginSlugs := m.externalPluginIDsAsCSV()
|
||||
resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + m.cfg.BuildVersion)
|
||||
if err != nil {
|
||||
log.Debug("Failed to get plugins repo from grafana.com", "error", err.Error())
|
||||
return
|
||||
@@ -64,64 +45,40 @@ func (pm *PluginManager) checkForUpdates() {
|
||||
return
|
||||
}
|
||||
|
||||
gNetPlugins := []grafanaNetPlugin{}
|
||||
err = json.Unmarshal(body, &gNetPlugins)
|
||||
var gcomPlugins []gcomPlugin
|
||||
err = json.Unmarshal(body, &gcomPlugins)
|
||||
if err != nil {
|
||||
log.Debug("Failed to unmarshal plugin repo, reading response from grafana.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, plug := range pm.Plugins() {
|
||||
for _, gplug := range gNetPlugins {
|
||||
if gplug.Slug == plug.Id {
|
||||
plug.GrafanaNetVersion = gplug.Version
|
||||
for _, localP := range m.Plugins() {
|
||||
for _, gcomP := range gcomPlugins {
|
||||
if gcomP.Slug == localP.ID {
|
||||
localP.GrafanaComVersion = gcomP.Version
|
||||
|
||||
plugVersion, err1 := version.NewVersion(plug.Info.Version)
|
||||
gplugVersion, err2 := version.NewVersion(gplug.Version)
|
||||
plugVersion, err1 := version.NewVersion(localP.Info.Version)
|
||||
gplugVersion, err2 := version.NewVersion(gcomP.Version)
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
plug.GrafanaNetHasUpdate = plug.Info.Version != plug.GrafanaNetVersion
|
||||
localP.GrafanaComHasUpdate = localP.Info.Version != localP.GrafanaComVersion
|
||||
} else {
|
||||
plug.GrafanaNetHasUpdate = plugVersion.LessThan(gplugVersion)
|
||||
localP.GrafanaComHasUpdate = plugVersion.LessThan(gplugVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resp2, err := httpClient.Get("https://raw.githubusercontent.com/grafana/grafana/main/latest.json")
|
||||
if err != nil {
|
||||
log.Debug("Failed to get latest.json repo from github.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := resp2.Body.Close(); err != nil {
|
||||
pm.log.Warn("Failed to close response body", "err", err)
|
||||
func (m *PluginManager) externalPluginIDsAsCSV() string {
|
||||
var result []string
|
||||
for _, p := range m.plugins {
|
||||
if p.IsCorePlugin() {
|
||||
continue
|
||||
}
|
||||
}()
|
||||
body, err = ioutil.ReadAll(resp2.Body)
|
||||
if err != nil {
|
||||
log.Debug("Update check failed, reading response from github.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var latest gitHubLatest
|
||||
err = json.Unmarshal(body, &latest)
|
||||
if err != nil {
|
||||
log.Debug("Failed to unmarshal github.com latest, reading response from github.com", "error", err.Error())
|
||||
return
|
||||
result = append(result, p.ID)
|
||||
}
|
||||
|
||||
if strings.Contains(setting.BuildVersion, "-") {
|
||||
pm.grafanaLatestVersion = latest.Testing
|
||||
pm.grafanaHasUpdate = !strings.HasPrefix(setting.BuildVersion, latest.Testing)
|
||||
} else {
|
||||
pm.grafanaLatestVersion = latest.Stable
|
||||
pm.grafanaHasUpdate = latest.Stable != setting.BuildVersion
|
||||
}
|
||||
|
||||
currVersion, err1 := version.NewVersion(setting.BuildVersion)
|
||||
latestVersion, err2 := version.NewVersion(pm.grafanaLatestVersion)
|
||||
if err1 == nil && err2 == nil {
|
||||
pm.grafanaHasUpdate = currVersion.LessThan(latestVersion)
|
||||
}
|
||||
return strings.Join(result, ",")
|
||||
}
|
||||
|
||||
+119
-89
@@ -1,17 +1,14 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
)
|
||||
|
||||
const (
|
||||
PluginTypeApp = "app"
|
||||
PluginTypeDashboard = "dashboard"
|
||||
TypeDashboard = "dashboard"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -21,89 +18,70 @@ var (
|
||||
ErrPluginNotInstalled = errors.New("plugin is not installed")
|
||||
)
|
||||
|
||||
type PluginNotFoundError struct {
|
||||
type NotFoundError struct {
|
||||
PluginID string
|
||||
}
|
||||
|
||||
func (e PluginNotFoundError) Error() string {
|
||||
func (e NotFoundError) Error() string {
|
||||
return fmt.Sprintf("plugin with ID '%s' not found", e.PluginID)
|
||||
}
|
||||
|
||||
type DuplicatePluginError struct {
|
||||
type DuplicateError struct {
|
||||
PluginID string
|
||||
ExistingPluginDir string
|
||||
}
|
||||
|
||||
func (e DuplicatePluginError) Error() string {
|
||||
func (e DuplicateError) Error() string {
|
||||
return fmt.Sprintf("plugin with ID '%s' already exists in '%s'", e.PluginID, e.ExistingPluginDir)
|
||||
}
|
||||
|
||||
func (e DuplicatePluginError) Is(err error) bool {
|
||||
func (e DuplicateError) Is(err error) bool {
|
||||
// nolint:errorlint
|
||||
_, ok := err.(DuplicatePluginError)
|
||||
_, ok := err.(DuplicateError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// PluginLoader can load a plugin.
|
||||
type PluginLoader interface {
|
||||
// Load loads a plugin and returns it.
|
||||
Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) (interface{}, error)
|
||||
type SignatureError struct {
|
||||
PluginID string `json:"pluginId"`
|
||||
SignatureStatus SignatureStatus `json:"status"`
|
||||
}
|
||||
|
||||
// PluginBase is the base plugin type.
|
||||
type PluginBase struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Id string `json:"id"`
|
||||
Info PluginInfo `json:"info"`
|
||||
Dependencies PluginDependencies `json:"dependencies"`
|
||||
Includes []*PluginInclude `json:"includes"`
|
||||
Module string `json:"module"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
Category string `json:"category"`
|
||||
HideFromList bool `json:"hideFromList,omitempty"`
|
||||
Preload bool `json:"preload"`
|
||||
State PluginState `json:"state,omitempty"`
|
||||
Signature PluginSignatureStatus `json:"signature"`
|
||||
Backend bool `json:"backend"`
|
||||
|
||||
IncludedInAppId string `json:"-"`
|
||||
PluginDir string `json:"-"`
|
||||
DefaultNavUrl string `json:"-"`
|
||||
IsCorePlugin bool `json:"-"`
|
||||
SignatureType PluginSignatureType `json:"-"`
|
||||
SignatureOrg string `json:"-"`
|
||||
SignedFiles PluginFiles `json:"-"`
|
||||
|
||||
GrafanaNetVersion string `json:"-"`
|
||||
GrafanaNetHasUpdate bool `json:"-"`
|
||||
|
||||
Root *PluginBase
|
||||
}
|
||||
|
||||
func (p *PluginBase) IncludedInSignature(file string) bool {
|
||||
// permit Core plugin files
|
||||
if p.IsCorePlugin {
|
||||
return true
|
||||
func (e SignatureError) Error() string {
|
||||
switch e.SignatureStatus {
|
||||
case SignatureInvalid:
|
||||
return fmt.Sprintf("plugin '%s' has an invalid signature", e.PluginID)
|
||||
case SignatureModified:
|
||||
return fmt.Sprintf("plugin '%s' has an modified signature", e.PluginID)
|
||||
case SignatureUnsigned:
|
||||
return fmt.Sprintf("plugin '%s' has no signature", e.PluginID)
|
||||
case SignatureInternal, SignatureValid:
|
||||
return ""
|
||||
}
|
||||
|
||||
// permit when no signed files (no MANIFEST)
|
||||
if p.SignedFiles == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, exists := p.SignedFiles[file]; !exists {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return fmt.Sprintf("plugin '%s' has an unknown signature state", e.PluginID)
|
||||
}
|
||||
|
||||
type PluginDependencies struct {
|
||||
GrafanaVersion string `json:"grafanaVersion"`
|
||||
Plugins []PluginDependencyItem `json:"plugins"`
|
||||
func (e SignatureError) AsErrorCode() ErrorCode {
|
||||
switch e.SignatureStatus {
|
||||
case SignatureInvalid:
|
||||
return signatureInvalid
|
||||
case SignatureModified:
|
||||
return signatureModified
|
||||
case SignatureUnsigned:
|
||||
return signatureMissing
|
||||
case SignatureInternal, SignatureValid:
|
||||
return ""
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
type PluginInclude struct {
|
||||
type Dependencies struct {
|
||||
GrafanaVersion string `json:"grafanaVersion"`
|
||||
Plugins []Dependency `json:"plugins"`
|
||||
}
|
||||
|
||||
type Includes struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
@@ -115,10 +93,10 @@ type PluginInclude struct {
|
||||
Icon string `json:"icon"`
|
||||
UID string `json:"uid"`
|
||||
|
||||
Id string `json:"-"`
|
||||
ID string `json:"-"`
|
||||
}
|
||||
|
||||
func (e PluginInclude) GetSlugOrUIDLink() string {
|
||||
func (e Includes) GetSlugOrUIDLink() string {
|
||||
if len(e.UID) > 0 {
|
||||
return "/d/" + e.UID
|
||||
} else {
|
||||
@@ -126,57 +104,109 @@ func (e PluginInclude) GetSlugOrUIDLink() string {
|
||||
}
|
||||
}
|
||||
|
||||
type PluginDependencyItem struct {
|
||||
type Dependency struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type PluginBuildInfo struct {
|
||||
type BuildInfo struct {
|
||||
Time int64 `json:"time,omitempty"`
|
||||
Repo string `json:"repo,omitempty"`
|
||||
Branch string `json:"branch,omitempty"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
}
|
||||
|
||||
type PluginInfo struct {
|
||||
Author PluginInfoLink `json:"author"`
|
||||
Description string `json:"description"`
|
||||
Links []PluginInfoLink `json:"links"`
|
||||
Logos PluginLogos `json:"logos"`
|
||||
Build PluginBuildInfo `json:"build"`
|
||||
Screenshots []PluginScreenshots `json:"screenshots"`
|
||||
Version string `json:"version"`
|
||||
Updated string `json:"updated"`
|
||||
type Info struct {
|
||||
Author InfoLink `json:"author"`
|
||||
Description string `json:"description"`
|
||||
Links []InfoLink `json:"links"`
|
||||
Logos Logos `json:"logos"`
|
||||
Build BuildInfo `json:"build"`
|
||||
Screenshots []Screenshots `json:"screenshots"`
|
||||
Version string `json:"version"`
|
||||
Updated string `json:"updated"`
|
||||
}
|
||||
|
||||
type PluginInfoLink struct {
|
||||
type InfoLink struct {
|
||||
Name string `json:"name"`
|
||||
Url string `json:"url"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type PluginLogos struct {
|
||||
type Logos struct {
|
||||
Small string `json:"small"`
|
||||
Large string `json:"large"`
|
||||
}
|
||||
|
||||
type PluginScreenshots struct {
|
||||
Path string `json:"path"`
|
||||
type Screenshots struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type PluginStaticRoute struct {
|
||||
type StaticRoute struct {
|
||||
PluginID string
|
||||
Directory string
|
||||
PluginId string
|
||||
}
|
||||
|
||||
type EnabledPlugins struct {
|
||||
Panels []*PanelPlugin
|
||||
DataSources map[string]*DataSourcePlugin
|
||||
Apps []*AppPlugin
|
||||
type SignatureStatus string
|
||||
|
||||
func (ss SignatureStatus) IsValid() bool {
|
||||
return ss == SignatureValid
|
||||
}
|
||||
|
||||
type UpdateInfo struct {
|
||||
PluginZipURL string
|
||||
func (ss SignatureStatus) IsInternal() bool {
|
||||
return ss == SignatureInternal
|
||||
}
|
||||
|
||||
const (
|
||||
SignatureInternal SignatureStatus = "internal" // core plugin, no signature
|
||||
SignatureValid SignatureStatus = "valid" // signed and accurate MANIFEST
|
||||
SignatureInvalid SignatureStatus = "invalid" // invalid signature
|
||||
SignatureModified SignatureStatus = "modified" // valid signature, but content mismatch
|
||||
SignatureUnsigned SignatureStatus = "unsigned" // no MANIFEST file
|
||||
)
|
||||
|
||||
type ReleaseState string
|
||||
|
||||
const (
|
||||
AlphaRelease ReleaseState = "alpha"
|
||||
)
|
||||
|
||||
type SignatureType string
|
||||
|
||||
const (
|
||||
GrafanaSignature SignatureType = "grafana"
|
||||
PrivateSignature SignatureType = "private"
|
||||
)
|
||||
|
||||
type PluginFiles map[string]struct{}
|
||||
|
||||
type Signature struct {
|
||||
Status SignatureStatus
|
||||
Type SignatureType
|
||||
SigningOrg string
|
||||
Files PluginFiles
|
||||
}
|
||||
|
||||
type PluginMetaDTO struct {
|
||||
JSONData
|
||||
|
||||
Signature SignatureStatus `json:"signature"`
|
||||
|
||||
Module string `json:"module"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
}
|
||||
|
||||
const (
|
||||
signatureMissing ErrorCode = "signatureMissing"
|
||||
signatureModified ErrorCode = "signatureModified"
|
||||
signatureInvalid ErrorCode = "signatureInvalid"
|
||||
)
|
||||
|
||||
type ErrorCode string
|
||||
|
||||
type Error struct {
|
||||
ErrorCode `json:"errorCode"`
|
||||
PluginID string `json:"pluginId,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
)
|
||||
|
||||
type PanelPlugin struct {
|
||||
FrontendPluginBase
|
||||
SkipDataQuery bool `json:"skipDataQuery"`
|
||||
}
|
||||
|
||||
func (p *PanelPlugin) Load(decoder *json.Decoder, base *PluginBase, backendPluginManager backendplugin.Manager) (
|
||||
interface{}, error) {
|
||||
if err := decoder.Decode(p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.Id == "grafana-piechart-panel" {
|
||||
p.Name = "Pie Chart (old)"
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/localcache"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
@@ -20,13 +21,13 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
func ProvideService(bus bus.Bus, cacheService *localcache.CacheService, pluginManager plugins.Manager,
|
||||
func ProvideService(bus bus.Bus, cacheService *localcache.CacheService, pluginStore plugins.Store,
|
||||
dataSourceCache datasources.CacheService, encryptionService encryption.Service,
|
||||
pluginSettingsService *pluginsettings.Service) *Provider {
|
||||
return &Provider{
|
||||
Bus: bus,
|
||||
CacheService: cacheService,
|
||||
PluginManager: pluginManager,
|
||||
pluginStore: pluginStore,
|
||||
DataSourceCache: dataSourceCache,
|
||||
EncryptionService: encryptionService,
|
||||
PluginSettingsService: pluginSettingsService,
|
||||
@@ -37,7 +38,7 @@ func ProvideService(bus bus.Bus, cacheService *localcache.CacheService, pluginMa
|
||||
type Provider struct {
|
||||
Bus bus.Bus
|
||||
CacheService *localcache.CacheService
|
||||
PluginManager plugins.Manager
|
||||
pluginStore plugins.Store
|
||||
DataSourceCache datasources.CacheService
|
||||
EncryptionService encryption.Service
|
||||
PluginSettingsService *pluginsettings.Service
|
||||
@@ -49,7 +50,7 @@ type Provider struct {
|
||||
// returned context.
|
||||
func (p *Provider) Get(pluginID string, datasourceUID string, user *models.SignedInUser, skipCache bool) (backend.PluginContext, bool, error) {
|
||||
pc := backend.PluginContext{}
|
||||
plugin := p.PluginManager.GetPlugin(pluginID)
|
||||
plugin := p.pluginStore.Plugin(pluginID)
|
||||
if plugin == nil {
|
||||
return pc, false, nil
|
||||
}
|
||||
@@ -76,7 +77,7 @@ func (p *Provider) Get(pluginID string, datasourceUID string, user *models.Signe
|
||||
|
||||
pCtx := backend.PluginContext{
|
||||
OrgID: user.OrgId,
|
||||
PluginID: plugin.Id,
|
||||
PluginID: plugin.ID,
|
||||
User: adapters.BackendUserFromSignedInUser(user),
|
||||
AppInstanceSettings: &backend.AppInstanceSettings{
|
||||
JSONData: jsonData,
|
||||
|
||||
@@ -6,15 +6,15 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
func ProvideService(dataService *tsdb.Service, pluginManager plugins.Manager, sqlStore *sqlstore.SQLStore) *Service {
|
||||
func ProvideService(pluginStore plugins.Store, pluginDashboardManager plugins.PluginDashboardManager,
|
||||
sqlStore *sqlstore.SQLStore) *Service {
|
||||
s := &Service{
|
||||
DataService: dataService,
|
||||
PluginManager: pluginManager,
|
||||
SQLStore: sqlStore,
|
||||
logger: log.New("plugindashboards"),
|
||||
sqlStore: sqlStore,
|
||||
pluginStore: pluginStore,
|
||||
pluginDashboardManager: pluginDashboardManager,
|
||||
logger: log.New("plugindashboards"),
|
||||
}
|
||||
bus.AddEventListener(s.handlePluginStateChanged)
|
||||
s.updateAppDashboards()
|
||||
@@ -22,9 +22,9 @@ func ProvideService(dataService *tsdb.Service, pluginManager plugins.Manager, sq
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
DataService *tsdb.Service
|
||||
PluginManager plugins.Manager
|
||||
SQLStore *sqlstore.SQLStore
|
||||
sqlStore *sqlstore.SQLStore
|
||||
pluginStore plugins.Store
|
||||
pluginDashboardManager plugins.PluginDashboardManager
|
||||
|
||||
logger log.Logger
|
||||
}
|
||||
@@ -32,7 +32,7 @@ type Service struct {
|
||||
func (s *Service) updateAppDashboards() {
|
||||
s.logger.Debug("Looking for app dashboard updates")
|
||||
|
||||
pluginSettings, err := s.SQLStore.GetPluginSettings(0)
|
||||
pluginSettings, err := s.sqlStore.GetPluginSettings(0)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get all plugin settings", "error", err)
|
||||
return
|
||||
@@ -44,7 +44,7 @@ func (s *Service) updateAppDashboards() {
|
||||
continue
|
||||
}
|
||||
|
||||
if pluginDef := s.PluginManager.GetPlugin(pluginSetting.PluginId); pluginDef != nil {
|
||||
if pluginDef := s.pluginStore.Plugin(pluginSetting.PluginId); pluginDef != nil {
|
||||
if pluginDef.Info.Version != pluginSetting.PluginVersion {
|
||||
s.syncPluginDashboards(pluginDef, pluginSetting.OrgId)
|
||||
}
|
||||
@@ -52,11 +52,11 @@ func (s *Service) updateAppDashboards() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) syncPluginDashboards(pluginDef *plugins.PluginBase, orgID int64) {
|
||||
s.logger.Info("Syncing plugin dashboards to DB", "pluginId", pluginDef.Id)
|
||||
func (s *Service) syncPluginDashboards(pluginDef *plugins.Plugin, orgID int64) {
|
||||
s.logger.Info("Syncing plugin dashboards to DB", "pluginId", pluginDef.ID)
|
||||
|
||||
// Get plugin dashboards
|
||||
dashboards, err := s.PluginManager.GetPluginDashboards(orgID, pluginDef.Id)
|
||||
dashboards, err := s.pluginDashboardManager.GetPluginDashboards(orgID, pluginDef.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to load app dashboards", "error", err)
|
||||
return
|
||||
@@ -66,11 +66,11 @@ func (s *Service) syncPluginDashboards(pluginDef *plugins.PluginBase, orgID int6
|
||||
for _, dash := range dashboards {
|
||||
// remove removed ones
|
||||
if dash.Removed {
|
||||
s.logger.Info("Deleting plugin dashboard", "pluginId", pluginDef.Id, "dashboard", dash.Slug)
|
||||
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)
|
||||
s.logger.Error("Failed to auto update app dashboard", "pluginId", pluginDef.ID, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -80,14 +80,14 @@ func (s *Service) syncPluginDashboards(pluginDef *plugins.PluginBase, orgID int6
|
||||
// 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)
|
||||
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}
|
||||
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
|
||||
@@ -109,7 +109,7 @@ func (s *Service) handlePluginStateChanged(event *models.PluginStateChangedEvent
|
||||
s.logger.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled)
|
||||
|
||||
if event.Enabled {
|
||||
s.syncPluginDashboards(s.PluginManager.GetPlugin(event.PluginId), event.OrgId)
|
||||
s.syncPluginDashboards(s.pluginStore.Plugin(event.PluginId), event.OrgId)
|
||||
} else {
|
||||
query := models.GetDashboardsByPluginIdQuery{PluginId: event.PluginId, OrgId: event.OrgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
@@ -129,14 +129,14 @@ func (s *Service) handlePluginStateChanged(event *models.PluginStateChangedEvent
|
||||
}
|
||||
|
||||
func (s *Service) autoUpdateAppDashboard(pluginDashInfo *plugins.PluginDashboardInfoDTO, orgID int64) error {
|
||||
dash, err := s.PluginManager.LoadPluginDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path)
|
||||
dash, err := s.pluginDashboardManager.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)
|
||||
_, _, err = s.pluginDashboardManager.ImportDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path, orgID, 0, dash.Data, true,
|
||||
nil, user)
|
||||
return err
|
||||
}
|
||||
|
||||
+347
-1
@@ -1,2 +1,348 @@
|
||||
// Package plugins contains plugin related logic.
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"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/plugins/backendplugin/pluginextensionv2"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
JSONData
|
||||
|
||||
PluginDir string
|
||||
Class Class
|
||||
|
||||
// App fields
|
||||
IncludedInAppID string
|
||||
DefaultNavURL string
|
||||
Pinned bool
|
||||
|
||||
// Signature fields
|
||||
Signature SignatureStatus
|
||||
SignatureType SignatureType
|
||||
SignatureOrg string
|
||||
Parent *Plugin
|
||||
Children []*Plugin
|
||||
SignedFiles PluginFiles
|
||||
SignatureError *SignatureError
|
||||
|
||||
// GCOM update checker fields
|
||||
GrafanaComVersion string
|
||||
GrafanaComHasUpdate bool
|
||||
|
||||
// SystemJS fields
|
||||
Module string
|
||||
BaseURL string
|
||||
|
||||
Renderer pluginextensionv2.RendererPlugin
|
||||
client backendplugin.Plugin
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
// JSONData represents the plugin's plugin.json
|
||||
type JSONData struct {
|
||||
// Common settings
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
Name string `json:"name"`
|
||||
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"`
|
||||
|
||||
// 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)
|
||||
Executable string `json:"executable,omitempty"`
|
||||
}
|
||||
|
||||
// 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 models.RoleType `json:"reqRole"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
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 {
|
||||
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) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
pluginClient, ok := p.Client()
|
||||
if !ok {
|
||||
return nil, backendplugin.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 backendplugin.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, backendplugin.ErrPluginUnavailable
|
||||
}
|
||||
return pluginClient.CheckHealth(ctx, req)
|
||||
}
|
||||
|
||||
func (p *Plugin) CollectMetrics(ctx context.Context) (*backend.CollectMetricsResult, error) {
|
||||
pluginClient, ok := p.Client()
|
||||
if !ok {
|
||||
return nil, backendplugin.ErrPluginUnavailable
|
||||
}
|
||||
return pluginClient.CollectMetrics(ctx)
|
||||
}
|
||||
|
||||
func (p *Plugin) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
|
||||
pluginClient, ok := p.Client()
|
||||
if !ok {
|
||||
return nil, backendplugin.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, backendplugin.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 backendplugin.ErrPluginUnavailable
|
||||
}
|
||||
return pluginClient.RunStream(ctx, req, sender)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type PluginClient interface {
|
||||
backend.QueryDataHandler
|
||||
backend.CollectMetricsHandler
|
||||
backend.CheckHealthHandler
|
||||
backend.CallResourceHandler
|
||||
backend.StreamHandler
|
||||
}
|
||||
|
||||
func (p *Plugin) StaticRoute() *StaticRoute {
|
||||
if p.IsCorePlugin() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &StaticRoute{Directory: p.PluginDir, PluginID: p.ID}
|
||||
}
|
||||
|
||||
func (p *Plugin) IsRenderer() bool {
|
||||
return p.Type == "renderer"
|
||||
}
|
||||
|
||||
func (p *Plugin) IsDataSource() bool {
|
||||
return p.Type == "datasource"
|
||||
}
|
||||
|
||||
func (p *Plugin) IsPanel() bool {
|
||||
return p.Type == "panel"
|
||||
}
|
||||
|
||||
func (p *Plugin) IsApp() bool {
|
||||
return p.Type == "app"
|
||||
}
|
||||
|
||||
func (p *Plugin) IsCorePlugin() bool {
|
||||
return p.Class == Core
|
||||
}
|
||||
|
||||
func (p *Plugin) IsBundledPlugin() bool {
|
||||
return p.Class == Bundled
|
||||
}
|
||||
|
||||
func (p *Plugin) IsExternalPlugin() bool {
|
||||
return p.Class == External
|
||||
}
|
||||
|
||||
func (p *Plugin) SupportsStreaming() bool {
|
||||
pluginClient, ok := p.Client()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok = pluginClient.(backend.StreamHandler)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (p *Plugin) IncludedInSignature(file string) bool {
|
||||
// permit Core plugin files
|
||||
if p.IsCorePlugin() {
|
||||
return true
|
||||
}
|
||||
|
||||
// permit when no signed files (no MANIFEST)
|
||||
if p.SignedFiles == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, exists := p.SignedFiles[file]; !exists {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type Class string
|
||||
|
||||
const (
|
||||
Core Class = "core"
|
||||
Bundled Class = "bundled"
|
||||
External Class = "external"
|
||||
)
|
||||
|
||||
var PluginTypes = []Type{
|
||||
DataSource,
|
||||
Panel,
|
||||
App,
|
||||
Renderer,
|
||||
}
|
||||
|
||||
type Type string
|
||||
|
||||
const (
|
||||
DataSource Type = "datasource"
|
||||
Panel Type = "panel"
|
||||
App Type = "app"
|
||||
Renderer Type = "renderer"
|
||||
)
|
||||
|
||||
func (pt Type) IsValid() bool {
|
||||
switch pt {
|
||||
case DataSource, Panel, App, Renderer:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/grpcplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
type RendererPlugin struct {
|
||||
FrontendPluginBase
|
||||
|
||||
Executable string `json:"executable,omitempty"`
|
||||
GrpcPluginV2 pluginextensionv2.RendererPlugin
|
||||
backendPluginManager backendplugin.Manager
|
||||
}
|
||||
|
||||
func (r *RendererPlugin) Load(decoder *json.Decoder, base *PluginBase,
|
||||
backendPluginManager backendplugin.Manager) (interface{}, error) {
|
||||
if err := decoder.Decode(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.backendPluginManager = backendPluginManager
|
||||
|
||||
cmd := ComposePluginStartCommand("plugin_start")
|
||||
fullpath := filepath.Join(base.PluginDir, cmd)
|
||||
factory := grpcplugin.NewRendererPlugin(r.Id, fullpath, r.onPluginStart)
|
||||
if err := backendPluginManager.Register(r.Id, factory); err != nil {
|
||||
return nil, errutil.Wrapf(err, "failed to register backend plugin")
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *RendererPlugin) Start(ctx context.Context) error {
|
||||
if err := r.backendPluginManager.StartPlugin(ctx, r.Id); err != nil {
|
||||
return errutil.Wrapf(err, "Failed to start renderer plugin")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RendererPlugin) onPluginStart(pluginID string, renderer pluginextensionv2.RendererPlugin, logger log.Logger) error {
|
||||
r.GrpcPluginV2 = renderer
|
||||
return nil
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
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 PluginFiles map[string]struct{}
|
||||
|
||||
type PluginSignatureState struct {
|
||||
Status PluginSignatureStatus
|
||||
Type PluginSignatureType
|
||||
SigningOrg string
|
||||
Files PluginFiles
|
||||
}
|
||||
Reference in New Issue
Block a user