mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
f4282571c7
commit
b80fbe03f0
@ -49,7 +49,7 @@ cp -r devenv $RUNDIR
|
||||
echo -e "Starting Grafana Server port $PORT"
|
||||
|
||||
$RUNDIR/bin/grafana-server \
|
||||
--homepath=$RUNDIR \
|
||||
--homepath=$HOME_PATH \
|
||||
--pidfile=$RUNDIR/pid \
|
||||
cfg:server.http_port=$PORT \
|
||||
cfg:server.router_logging=1 \
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
DEFAULT_RUNDIR=e2e/tmp
|
||||
RUNDIR=${RUNDIR:-$DEFAULT_RUNDIR}
|
||||
HOME_PATH=$PWD/$DEFAULT_RUNDIR
|
||||
PIDFILE=$RUNDIR/pid
|
||||
DEFAULT_PACKAGE_FILE=dist/grafana-*linux-amd64.tar.gz
|
||||
PROV_DIR=$RUNDIR/conf/provisioning
|
||||
|
@ -141,6 +141,7 @@ import u1132 from '!!raw-loader!../../../../../public/img/icons/mono/heart.svg';
|
||||
import u1133 from '!!raw-loader!../../../../../public/img/icons/mono/heart-break.svg';
|
||||
import u1134 from '!!raw-loader!../../../../../public/img/icons/mono/panel-add.svg';
|
||||
import u1135 from '!!raw-loader!../../../../../public/img/icons/mono/library-panel.svg';
|
||||
import u1136 from '!!raw-loader!../../../../../public/img/icons/unicons/capture.svg';
|
||||
|
||||
function cacheItem(content: string, path: string) {
|
||||
cacheStore[iconRoot + path] = { content, status: 'loaded', queue: [] };
|
||||
@ -291,4 +292,5 @@ export function initIconCache() {
|
||||
cacheItem(u1133, 'mono/heart-break.svg');
|
||||
cacheItem(u1134, 'mono/panel-add.svg');
|
||||
cacheItem(u1135, 'mono/library-panel.svg');
|
||||
cacheItem(u1136, 'unicons/capture.svg');
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ export const getAvailableIcons = () =>
|
||||
'calculator-alt',
|
||||
'calendar-alt',
|
||||
'camera',
|
||||
'capture',
|
||||
'channel-add',
|
||||
'chart-line',
|
||||
'check',
|
||||
|
@ -502,7 +502,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Delete("/api/snapshots/:key", reqEditorRole, routing.Wrap(DeleteDashboardSnapshot))
|
||||
|
||||
// Frontend logs
|
||||
sourceMapStore := frontendlogging.NewSourceMapStore(hs.Cfg, hs.PluginManager, frontendlogging.ReadSourceMapFromFS)
|
||||
sourceMapStore := frontendlogging.NewSourceMapStore(hs.Cfg, hs.pluginStaticRouteResolver, frontendlogging.ReadSourceMapFromFS)
|
||||
r.Post("/log", middleware.RateLimit(hs.Cfg.Sentry.EndpointRPS, hs.Cfg.Sentry.EndpointBurst, time.Now),
|
||||
bind(frontendlogging.FrontendSentryEvent{}), routing.Wrap(NewFrontendLogMessageHandler(sourceMapStore)))
|
||||
}
|
||||
|
@ -32,9 +32,9 @@ func (hs *HTTPServer) initAppPluginRoutes(r *web.Mux) {
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
for _, plugin := range hs.PluginManager.Apps() {
|
||||
for _, plugin := range hs.pluginStore.Plugins(plugins.App) {
|
||||
for _, route := range plugin.Routes {
|
||||
url := util.JoinURLFragments("/api/plugin-proxy/"+plugin.Id, route.Path)
|
||||
url := util.JoinURLFragments("/api/plugin-proxy/"+plugin.ID, route.Path)
|
||||
handlers := make([]web.Handler, 0)
|
||||
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{
|
||||
ReqSignedIn: true,
|
||||
@ -47,7 +47,7 @@ func (hs *HTTPServer) initAppPluginRoutes(r *web.Mux) {
|
||||
handlers = append(handlers, middleware.RoleAuth(models.ROLE_EDITOR, models.ROLE_ADMIN))
|
||||
}
|
||||
}
|
||||
handlers = append(handlers, AppPluginRoute(route, plugin.Id, hs))
|
||||
handlers = append(handlers, AppPluginRoute(route, plugin.ID, hs))
|
||||
for _, method := range strings.Split(route.Method, ",") {
|
||||
r.Handle(strings.TrimSpace(method), url, handlers)
|
||||
}
|
||||
@ -56,7 +56,7 @@ func (hs *HTTPServer) initAppPluginRoutes(r *web.Mux) {
|
||||
}
|
||||
}
|
||||
|
||||
func AppPluginRoute(route *plugins.AppPluginRoute, appID string, hs *HTTPServer) web.Handler {
|
||||
func AppPluginRoute(route *plugins.Route, appID string, hs *HTTPServer) web.Handler {
|
||||
return func(c *models.ReqContext) {
|
||||
path := web.Params(c.Req)["*"]
|
||||
|
||||
|
@ -410,7 +410,7 @@ func (hs *HTTPServer) dashboardSaveErrorToApiResponse(err error) response.Respon
|
||||
if ok := errors.As(err, &pluginErr); ok {
|
||||
message := fmt.Sprintf("The dashboard belongs to plugin %s.", pluginErr.PluginId)
|
||||
// look up plugin name
|
||||
if pluginDef := hs.PluginManager.GetPlugin(pluginErr.PluginId); pluginDef != nil {
|
||||
if pluginDef := hs.pluginStore.Plugin(pluginErr.PluginId); pluginDef != nil {
|
||||
message = fmt.Sprintf("The dashboard belongs to plugin %s.", pluginDef.Name)
|
||||
}
|
||||
return response.JSON(412, util.DynMap{"status": "plugin-dashboard", "message": message})
|
||||
|
@ -40,7 +40,7 @@ func TestGetHomeDashboard(t *testing.T) {
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: cfg, Bus: bus.New(),
|
||||
PluginManager: &fakePluginManager{},
|
||||
pluginStore: &fakePluginStore{},
|
||||
}
|
||||
hs.Bus.AddHandlerCtx(func(_ context.Context, query *models.GetPreferencesWithDefaultsQuery) error {
|
||||
query.Result = &models.Preferences{
|
||||
@ -1115,7 +1115,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
|
||||
QuotaService: "a.QuotaService{
|
||||
Cfg: cfg,
|
||||
},
|
||||
PluginManager: &fakePluginManager{},
|
||||
pluginStore: &fakePluginStore{},
|
||||
LibraryPanelService: &mockLibraryPanelService{},
|
||||
LibraryElementService: &mockLibraryElementService{},
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ func (hs *HTTPServer) GetDataSources(c *models.ReqContext) response.Response {
|
||||
ReadOnly: ds.ReadOnly,
|
||||
}
|
||||
|
||||
if plugin := hs.PluginManager.GetDataSource(ds.Type); plugin != nil {
|
||||
if plugin := hs.pluginStore.Plugin(ds.Type); plugin != nil {
|
||||
dsItem.TypeLogoUrl = plugin.Info.Logos.Small
|
||||
dsItem.TypeName = plugin.Name
|
||||
} else {
|
||||
@ -379,8 +379,7 @@ func (hs *HTTPServer) CallDatasourceResource(c *models.ReqContext) {
|
||||
return
|
||||
}
|
||||
|
||||
// find plugin
|
||||
plugin := hs.PluginManager.GetDataSource(ds.Type)
|
||||
plugin := hs.pluginStore.Plugin(ds.Type)
|
||||
if plugin == nil {
|
||||
c.JsonApiErr(500, "Unable to find datasource plugin", err)
|
||||
return
|
||||
@ -394,10 +393,10 @@ func (hs *HTTPServer) CallDatasourceResource(c *models.ReqContext) {
|
||||
pCtx := backend.PluginContext{
|
||||
User: adapters.BackendUserFromSignedInUser(c.SignedInUser),
|
||||
OrgID: c.OrgId,
|
||||
PluginID: plugin.Id,
|
||||
PluginID: plugin.ID,
|
||||
DataSourceInstanceSettings: dsInstanceSettings,
|
||||
}
|
||||
hs.BackendPluginManager.CallResource(pCtx, c, web.Params(c.Req)["*"])
|
||||
hs.pluginClient.CallResource(pCtx, c, web.Params(c.Req)["*"])
|
||||
}
|
||||
|
||||
func convertModelToDtos(ds *models.DataSource) dtos.DataSource {
|
||||
@ -445,7 +444,7 @@ func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) response.Respo
|
||||
return response.Error(500, "Unable to load datasource metadata", err)
|
||||
}
|
||||
|
||||
plugin := hs.PluginManager.GetDataSource(ds.Type)
|
||||
plugin := hs.pluginStore.Plugin(ds.Type)
|
||||
if plugin == nil {
|
||||
return response.Error(500, "Unable to find datasource plugin", err)
|
||||
}
|
||||
@ -454,14 +453,16 @@ func (hs *HTTPServer) CheckDatasourceHealth(c *models.ReqContext) response.Respo
|
||||
if err != nil {
|
||||
return response.Error(500, "Unable to get datasource model", err)
|
||||
}
|
||||
pCtx := backend.PluginContext{
|
||||
User: adapters.BackendUserFromSignedInUser(c.SignedInUser),
|
||||
OrgID: c.OrgId,
|
||||
PluginID: plugin.Id,
|
||||
DataSourceInstanceSettings: dsInstanceSettings,
|
||||
req := &backend.CheckHealthRequest{
|
||||
PluginContext: backend.PluginContext{
|
||||
User: adapters.BackendUserFromSignedInUser(c.SignedInUser),
|
||||
OrgID: c.OrgId,
|
||||
PluginID: plugin.ID,
|
||||
DataSourceInstanceSettings: dsInstanceSettings,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := hs.BackendPluginManager.CheckHealth(c.Req.Context(), pCtx)
|
||||
resp, err := hs.pluginClient.CheckHealth(c.Req.Context(), req)
|
||||
if err != nil {
|
||||
return translatePluginRequestErrorToAPIError(err)
|
||||
}
|
||||
|
@ -42,9 +42,9 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
|
||||
|
||||
// handler func being tested
|
||||
hs := &HTTPServer{
|
||||
Bus: bus.GetBus(),
|
||||
Cfg: setting.NewCfg(),
|
||||
PluginManager: &fakePluginManager{},
|
||||
Bus: bus.GetBus(),
|
||||
Cfg: setting.NewCfg(),
|
||||
pluginStore: &fakePluginStore{},
|
||||
}
|
||||
sc.handlerFunc = hs.GetDataSources
|
||||
sc.fakeReq("GET", "/api/datasources").exec()
|
||||
@ -63,9 +63,9 @@ func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
|
||||
"/api/datasources/name/12345", func(sc *scenarioContext) {
|
||||
// handler func being tested
|
||||
hs := &HTTPServer{
|
||||
Bus: bus.GetBus(),
|
||||
Cfg: setting.NewCfg(),
|
||||
PluginManager: &fakePluginManager{},
|
||||
Bus: bus.GetBus(),
|
||||
Cfg: setting.NewCfg(),
|
||||
pluginStore: &fakePluginStore{},
|
||||
}
|
||||
sc.handlerFunc = hs.DeleteDataSourceByName
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
|
@ -6,42 +6,42 @@ import (
|
||||
)
|
||||
|
||||
type PluginSetting struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Id string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Module string `json:"module"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
Info *plugins.PluginInfo `json:"info"`
|
||||
Includes []*plugins.PluginInclude `json:"includes"`
|
||||
Dependencies *plugins.PluginDependencies `json:"dependencies"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
DefaultNavUrl string `json:"defaultNavUrl"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Id string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Module string `json:"module"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
Info *plugins.Info `json:"info"`
|
||||
Includes []*plugins.Includes `json:"includes"`
|
||||
Dependencies *plugins.Dependencies `json:"dependencies"`
|
||||
JsonData map[string]interface{} `json:"jsonData"`
|
||||
DefaultNavUrl string `json:"defaultNavUrl"`
|
||||
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
HasUpdate bool `json:"hasUpdate"`
|
||||
State plugins.PluginState `json:"state"`
|
||||
Signature plugins.PluginSignatureStatus `json:"signature"`
|
||||
SignatureType plugins.PluginSignatureType `json:"signatureType"`
|
||||
SignatureOrg string `json:"signatureOrg"`
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
HasUpdate bool `json:"hasUpdate"`
|
||||
State plugins.ReleaseState `json:"state"`
|
||||
Signature plugins.SignatureStatus `json:"signature"`
|
||||
SignatureType plugins.SignatureType `json:"signatureType"`
|
||||
SignatureOrg string `json:"signatureOrg"`
|
||||
}
|
||||
|
||||
type PluginListItem struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Id string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Info *plugins.PluginInfo `json:"info"`
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
HasUpdate bool `json:"hasUpdate"`
|
||||
DefaultNavUrl string `json:"defaultNavUrl"`
|
||||
Category string `json:"category"`
|
||||
State plugins.PluginState `json:"state"`
|
||||
Signature plugins.PluginSignatureStatus `json:"signature"`
|
||||
SignatureType plugins.PluginSignatureType `json:"signatureType"`
|
||||
SignatureOrg string `json:"signatureOrg"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Id string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Info *plugins.Info `json:"info"`
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
HasUpdate bool `json:"hasUpdate"`
|
||||
DefaultNavUrl string `json:"defaultNavUrl"`
|
||||
Category string `json:"category"`
|
||||
State plugins.ReleaseState `json:"state"`
|
||||
Signature plugins.SignatureStatus `json:"signature"`
|
||||
SignatureType plugins.SignatureType `json:"signatureType"`
|
||||
SignatureOrg string `json:"signatureOrg"`
|
||||
}
|
||||
|
||||
type PluginList []PluginListItem
|
||||
|
@ -2,24 +2,32 @@ package api
|
||||
|
||||
import "github.com/grafana/grafana/pkg/plugins"
|
||||
|
||||
type fakePluginManager struct {
|
||||
plugins.Manager
|
||||
|
||||
staticRoutes []*plugins.PluginStaticRoute
|
||||
type fakePluginStore struct {
|
||||
plugins.Store
|
||||
}
|
||||
|
||||
func (pm *fakePluginManager) GetPlugin(id string) *plugins.PluginBase {
|
||||
func (ps *fakePluginStore) Plugin(pluginID string) *plugins.Plugin {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *fakePluginManager) GetDataSource(id string) *plugins.DataSourcePlugin {
|
||||
func (ps *fakePluginStore) Plugins(pluginType ...plugins.Type) []*plugins.Plugin {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *fakePluginManager) Renderer() *plugins.RendererPlugin {
|
||||
type fakeRendererManager struct {
|
||||
plugins.RendererManager
|
||||
}
|
||||
|
||||
func (ps *fakeRendererManager) Renderer() *plugins.Plugin {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *fakePluginManager) StaticRoutes() []*plugins.PluginStaticRoute {
|
||||
return pm.staticRoutes
|
||||
type fakePluginStaticRouteResolver struct {
|
||||
plugins.StaticRouteResolver
|
||||
|
||||
routes []*plugins.StaticRoute
|
||||
}
|
||||
|
||||
func (psrr *fakePluginStaticRouteResolver) Routes() []*plugins.StaticRoute {
|
||||
return psrr.routes
|
||||
}
|
||||
|
@ -71,11 +71,11 @@ func logSentryEventScenario(t *testing.T, desc string, event frontendlogging.Fro
|
||||
}
|
||||
|
||||
// fake plugin route so we will try to find a source map there
|
||||
pm := fakePluginManager{
|
||||
staticRoutes: []*plugins.PluginStaticRoute{
|
||||
pm := fakePluginStaticRouteResolver{
|
||||
routes: []*plugins.StaticRoute{
|
||||
{
|
||||
Directory: "/usr/local/telepathic-panel",
|
||||
PluginId: "telepathic",
|
||||
PluginID: "telepathic",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -47,14 +47,14 @@ type SourceMapStore struct {
|
||||
cache map[string]*sourceMap
|
||||
cfg *setting.Cfg
|
||||
readSourceMap ReadSourceMapFn
|
||||
pluginManager plugins.Manager
|
||||
routeResolver plugins.StaticRouteResolver
|
||||
}
|
||||
|
||||
func NewSourceMapStore(cfg *setting.Cfg, pluginManager plugins.Manager, readSourceMap ReadSourceMapFn) *SourceMapStore {
|
||||
func NewSourceMapStore(cfg *setting.Cfg, routeResolver plugins.StaticRouteResolver, readSourceMap ReadSourceMapFn) *SourceMapStore {
|
||||
return &SourceMapStore{
|
||||
cache: make(map[string]*sourceMap),
|
||||
cfg: cfg,
|
||||
pluginManager: pluginManager,
|
||||
routeResolver: routeResolver,
|
||||
readSourceMap: readSourceMap,
|
||||
}
|
||||
}
|
||||
@ -83,13 +83,13 @@ func (store *SourceMapStore) guessSourceMapLocation(sourceURL string) (*sourceMa
|
||||
}
|
||||
// if source comes from a plugin, look in plugin dir
|
||||
} else if strings.HasPrefix(u.Path, "/public/plugins/") {
|
||||
for _, route := range store.pluginManager.StaticRoutes() {
|
||||
pluginPrefix := filepath.Join("/public/plugins/", route.PluginId)
|
||||
for _, route := range store.routeResolver.Routes() {
|
||||
pluginPrefix := filepath.Join("/public/plugins/", route.PluginID)
|
||||
if strings.HasPrefix(u.Path, pluginPrefix) {
|
||||
return &sourceMapLocation{
|
||||
dir: route.Directory,
|
||||
path: u.Path[len(pluginPrefix):] + ".map",
|
||||
pluginID: route.PluginId,
|
||||
pluginID: route.PluginID,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins *plugins.EnabledPlugins) (map[string]interface{}, error) {
|
||||
func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins EnabledPlugins) (map[string]interface{}, error) {
|
||||
orgDataSources := make([]*models.DataSource, 0)
|
||||
|
||||
if c.OrgId != 0 {
|
||||
@ -61,12 +61,19 @@ func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins *plu
|
||||
"access": ds.Access,
|
||||
}
|
||||
|
||||
meta, exists := enabledPlugins.DataSources[ds.Type]
|
||||
meta, exists := enabledPlugins.Get(plugins.DataSource, ds.Type)
|
||||
if !exists {
|
||||
log.Error("Could not find plugin definition for data source", "datasource_type", ds.Type)
|
||||
continue
|
||||
}
|
||||
dsMap["meta"] = meta
|
||||
dsMap["preload"] = meta.Preload
|
||||
dsMap["module"] = meta.Module
|
||||
dsMap["meta"] = &plugins.PluginMetaDTO{
|
||||
JSONData: meta.JSONData,
|
||||
Signature: meta.Signature,
|
||||
Module: meta.Module,
|
||||
BaseURL: meta.BaseURL,
|
||||
}
|
||||
|
||||
jsonData := ds.JsonData
|
||||
if jsonData == nil {
|
||||
@ -113,12 +120,17 @@ func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins *plu
|
||||
|
||||
// add data sources that are built in (meaning they are not added via data sources page, nor have any entry in
|
||||
// the datasource table)
|
||||
for _, ds := range hs.PluginManager.DataSources() {
|
||||
for _, ds := range hs.pluginStore.Plugins(plugins.DataSource) {
|
||||
if ds.BuiltIn {
|
||||
info := map[string]interface{}{
|
||||
"type": ds.Type,
|
||||
"name": ds.Name,
|
||||
"meta": hs.PluginManager.GetDataSource(ds.Id),
|
||||
"meta": &plugins.PluginMetaDTO{
|
||||
JSONData: ds.JSONData,
|
||||
Signature: ds.Signature,
|
||||
Module: ds.Module,
|
||||
BaseURL: ds.BaseURL,
|
||||
},
|
||||
}
|
||||
if ds.Name == grafanads.DatasourceName {
|
||||
info["id"] = grafanads.DatasourceID
|
||||
@ -133,13 +145,13 @@ func (hs *HTTPServer) getFSDataSources(c *models.ReqContext, enabledPlugins *plu
|
||||
|
||||
// getFrontendSettingsMap returns a json object with all the settings needed for front end initialisation.
|
||||
func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]interface{}, error) {
|
||||
enabledPlugins, err := hs.PluginManager.GetEnabledPlugins(c.OrgId)
|
||||
enabledPlugins, err := hs.enabledPlugins(c.OrgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginsToPreload := []string{}
|
||||
for _, app := range enabledPlugins.Apps {
|
||||
for _, app := range enabledPlugins[plugins.App] {
|
||||
if app.Preload {
|
||||
pluginsToPreload = append(pluginsToPreload, app.Module)
|
||||
}
|
||||
@ -157,15 +169,15 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
defaultDS = n
|
||||
}
|
||||
|
||||
meta := dsM["meta"].(*plugins.DataSourcePlugin)
|
||||
if meta.Preload {
|
||||
pluginsToPreload = append(pluginsToPreload, meta.Module)
|
||||
module, _ := dsM["module"].(string)
|
||||
if preload, _ := dsM["preload"].(bool); preload && module != "" {
|
||||
pluginsToPreload = append(pluginsToPreload, module)
|
||||
}
|
||||
}
|
||||
|
||||
panels := map[string]interface{}{}
|
||||
for _, panel := range enabledPlugins.Panels {
|
||||
if panel.State == plugins.PluginStateAlpha && !hs.Cfg.PluginsEnableAlpha {
|
||||
for _, panel := range enabledPlugins[plugins.Panel] {
|
||||
if panel.State == plugins.AlphaRelease && !hs.Cfg.PluginsEnableAlpha {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -173,14 +185,14 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
pluginsToPreload = append(pluginsToPreload, panel.Module)
|
||||
}
|
||||
|
||||
panels[panel.Id] = map[string]interface{}{
|
||||
panels[panel.ID] = map[string]interface{}{
|
||||
"id": panel.ID,
|
||||
"module": panel.Module,
|
||||
"baseUrl": panel.BaseUrl,
|
||||
"baseUrl": panel.BaseURL,
|
||||
"name": panel.Name,
|
||||
"id": panel.Id,
|
||||
"info": panel.Info,
|
||||
"hideFromList": panel.HideFromList,
|
||||
"sort": getPanelSort(panel.Id),
|
||||
"sort": getPanelSort(panel.ID),
|
||||
"skipDataQuery": panel.SkipDataQuery,
|
||||
"state": panel.State,
|
||||
"signature": panel.Signature,
|
||||
@ -241,8 +253,8 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
"commit": commit,
|
||||
"buildstamp": buildstamp,
|
||||
"edition": hs.License.Edition(),
|
||||
"latestVersion": hs.PluginManager.GrafanaLatestVersion(),
|
||||
"hasUpdate": hs.PluginManager.GrafanaHasUpdate(),
|
||||
"latestVersion": hs.updateChecker.LatestGrafanaVersion(),
|
||||
"hasUpdate": hs.updateChecker.GrafanaUpdateAvailable(),
|
||||
"env": setting.Env,
|
||||
"isEnterprise": hs.License.HasValidLicense(),
|
||||
},
|
||||
@ -335,3 +347,96 @@ func (hs *HTTPServer) GetFrontendSettings(c *models.ReqContext) {
|
||||
|
||||
c.JSON(200, settings)
|
||||
}
|
||||
|
||||
// EnabledPlugins represents a mapping from plugin types (panel, data source, etc.) to plugin IDs to plugins
|
||||
// For example ["panel"] -> ["piechart"] -> {pie chart plugin instance}
|
||||
type EnabledPlugins map[plugins.Type]map[string]*plugins.Plugin
|
||||
|
||||
func (ep EnabledPlugins) Get(pluginType plugins.Type, pluginID string) (*plugins.Plugin, bool) {
|
||||
if _, exists := ep[pluginType][pluginID]; exists {
|
||||
return ep[pluginType][pluginID], true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) enabledPlugins(orgID int64) (EnabledPlugins, error) {
|
||||
ep := make(EnabledPlugins)
|
||||
|
||||
pluginSettingMap, err := hs.pluginSettings(orgID)
|
||||
if err != nil {
|
||||
return ep, err
|
||||
}
|
||||
|
||||
apps := make(map[string]*plugins.Plugin)
|
||||
for _, app := range hs.pluginStore.Plugins(plugins.App) {
|
||||
if b, exists := pluginSettingMap[app.ID]; exists {
|
||||
app.Pinned = b.Pinned
|
||||
apps[app.ID] = app
|
||||
}
|
||||
}
|
||||
ep[plugins.App] = apps
|
||||
|
||||
dataSources := make(map[string]*plugins.Plugin)
|
||||
for _, ds := range hs.pluginStore.Plugins(plugins.DataSource) {
|
||||
if _, exists := pluginSettingMap[ds.ID]; exists {
|
||||
dataSources[ds.ID] = ds
|
||||
}
|
||||
}
|
||||
ep[plugins.DataSource] = dataSources
|
||||
|
||||
panels := make(map[string]*plugins.Plugin)
|
||||
for _, p := range hs.pluginStore.Plugins(plugins.Panel) {
|
||||
if _, exists := pluginSettingMap[p.ID]; exists {
|
||||
panels[p.ID] = p
|
||||
}
|
||||
}
|
||||
ep[plugins.Panel] = panels
|
||||
|
||||
return ep, nil
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) pluginSettings(orgID int64) (map[string]*models.PluginSettingInfoDTO, error) {
|
||||
pluginSettings, err := hs.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 hs.pluginStore.Plugins() {
|
||||
// ignore entries that already exist
|
||||
if _, ok := pluginMap[pluginDef.ID]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// enabled by default
|
||||
opt := &models.PluginSettingInfoDTO{
|
||||
PluginId: pluginDef.ID,
|
||||
OrgId: orgID,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
// apps are disabled by default unless autoEnabled: true
|
||||
if p := hs.pluginStore.Plugin(pluginDef.ID); p != nil && p.IsApp() {
|
||||
opt.Enabled = p.AutoEnabled
|
||||
opt.Pinned = p.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
|
||||
}
|
||||
|
@ -8,11 +8,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/updatechecker"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -35,20 +35,18 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer
|
||||
}
|
||||
|
||||
sqlStore := sqlstore.InitTestDB(t)
|
||||
pm := &manager.PluginManager{Cfg: cfg, SQLStore: sqlStore}
|
||||
|
||||
r := &rendering.RenderingService{
|
||||
Cfg: cfg,
|
||||
PluginManager: pm,
|
||||
}
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: cfg,
|
||||
Bus: bus.GetBus(),
|
||||
License: &licensing.OSSLicensingService{Cfg: cfg},
|
||||
RenderService: r,
|
||||
Cfg: cfg,
|
||||
Bus: bus.GetBus(),
|
||||
License: &licensing.OSSLicensingService{Cfg: cfg},
|
||||
RenderService: &rendering.RenderingService{
|
||||
Cfg: cfg,
|
||||
RendererPluginManager: &fakeRendererManager{},
|
||||
},
|
||||
SQLStore: sqlStore,
|
||||
PluginManager: pm,
|
||||
pluginStore: &fakePluginStore{},
|
||||
updateChecker: &updatechecker.Service{},
|
||||
AccessControl: accesscontrolmock.New().WithDisabled(),
|
||||
}
|
||||
|
||||
@ -60,7 +58,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer
|
||||
return m, hs
|
||||
}
|
||||
|
||||
func TestHTTPServer_GetFrontendSettings_hideVersionAnonyomus(t *testing.T) {
|
||||
func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) {
|
||||
type buildInfo struct {
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
|
@ -13,8 +13,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
httpstatic "github.com/grafana/grafana/pkg/api/static"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@ -28,8 +26,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"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/manager"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugincontext"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
@ -52,8 +48,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/services/schemaloader"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||
"github.com/grafana/grafana/pkg/services/shorturls"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/updatechecker"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
@ -69,48 +67,52 @@ type HTTPServer struct {
|
||||
httpSrv *http.Server
|
||||
middlewares []web.Handler
|
||||
|
||||
PluginContextProvider *plugincontext.Provider
|
||||
RouteRegister routing.RouteRegister
|
||||
Bus bus.Bus
|
||||
RenderService rendering.Service
|
||||
Cfg *setting.Cfg
|
||||
SettingsProvider setting.Provider
|
||||
HooksService *hooks.HooksService
|
||||
CacheService *localcache.CacheService
|
||||
DataSourceCache datasources.CacheService
|
||||
AuthTokenService models.UserTokenService
|
||||
QuotaService *quota.QuotaService
|
||||
RemoteCacheService *remotecache.RemoteCache
|
||||
ProvisioningService provisioning.ProvisioningService
|
||||
Login login.Service
|
||||
License models.Licensing
|
||||
AccessControl accesscontrol.AccessControl
|
||||
BackendPluginManager backendplugin.Manager
|
||||
DataProxy *datasourceproxy.DataSourceProxyService
|
||||
PluginRequestValidator models.PluginRequestValidator
|
||||
PluginManager plugins.Manager
|
||||
SearchService *search.SearchService
|
||||
ShortURLService shorturls.Service
|
||||
Live *live.GrafanaLive
|
||||
LivePushGateway *pushhttp.Gateway
|
||||
ContextHandler *contexthandler.ContextHandler
|
||||
SQLStore *sqlstore.SQLStore
|
||||
DataService *tsdb.Service
|
||||
AlertEngine *alerting.AlertEngine
|
||||
LoadSchemaService *schemaloader.SchemaLoaderService
|
||||
AlertNG *ngalert.AlertNG
|
||||
LibraryPanelService librarypanels.Service
|
||||
LibraryElementService libraryelements.Service
|
||||
notificationService *notifications.NotificationService
|
||||
SocialService social.Service
|
||||
OAuthTokenService oauthtoken.OAuthTokenService
|
||||
Listener net.Listener
|
||||
EncryptionService encryption.Service
|
||||
DataSourcesService *datasources.Service
|
||||
cleanUpService *cleanup.CleanUpService
|
||||
tracingService *tracing.TracingService
|
||||
internalMetricsSvc *metrics.InternalMetricsService
|
||||
searchUsersService searchusers.Service
|
||||
PluginContextProvider *plugincontext.Provider
|
||||
RouteRegister routing.RouteRegister
|
||||
Bus bus.Bus
|
||||
RenderService rendering.Service
|
||||
Cfg *setting.Cfg
|
||||
SettingsProvider setting.Provider
|
||||
HooksService *hooks.HooksService
|
||||
CacheService *localcache.CacheService
|
||||
DataSourceCache datasources.CacheService
|
||||
AuthTokenService models.UserTokenService
|
||||
QuotaService *quota.QuotaService
|
||||
RemoteCacheService *remotecache.RemoteCache
|
||||
ProvisioningService provisioning.ProvisioningService
|
||||
Login login.Service
|
||||
License models.Licensing
|
||||
AccessControl accesscontrol.AccessControl
|
||||
DataProxy *datasourceproxy.DataSourceProxyService
|
||||
PluginRequestValidator models.PluginRequestValidator
|
||||
pluginClient plugins.Client
|
||||
pluginStore plugins.Store
|
||||
pluginDashboardManager plugins.PluginDashboardManager
|
||||
pluginStaticRouteResolver plugins.StaticRouteResolver
|
||||
pluginErrorResolver plugins.ErrorResolver
|
||||
SearchService *search.SearchService
|
||||
ShortURLService shorturls.Service
|
||||
Live *live.GrafanaLive
|
||||
LivePushGateway *pushhttp.Gateway
|
||||
ContextHandler *contexthandler.ContextHandler
|
||||
SQLStore *sqlstore.SQLStore
|
||||
DataService *tsdb.Service
|
||||
AlertEngine *alerting.AlertEngine
|
||||
LoadSchemaService *schemaloader.SchemaLoaderService
|
||||
AlertNG *ngalert.AlertNG
|
||||
LibraryPanelService librarypanels.Service
|
||||
LibraryElementService libraryelements.Service
|
||||
notificationService *notifications.NotificationService
|
||||
SocialService social.Service
|
||||
OAuthTokenService oauthtoken.OAuthTokenService
|
||||
Listener net.Listener
|
||||
EncryptionService encryption.Service
|
||||
DataSourcesService *datasources.Service
|
||||
cleanUpService *cleanup.CleanUpService
|
||||
tracingService *tracing.TracingService
|
||||
internalMetricsSvc *metrics.InternalMetricsService
|
||||
updateChecker *updatechecker.Service
|
||||
searchUsersService searchusers.Service
|
||||
}
|
||||
|
||||
type ServerOptions struct {
|
||||
@ -120,8 +122,10 @@ type ServerOptions struct {
|
||||
func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routing.RouteRegister, bus bus.Bus,
|
||||
renderService rendering.Service, licensing models.Licensing, hooksService *hooks.HooksService,
|
||||
cacheService *localcache.CacheService, sqlStore *sqlstore.SQLStore,
|
||||
dataService *tsdb.Service, alertEngine *alerting.AlertEngine, pluginRequestValidator models.PluginRequestValidator,
|
||||
pluginManager plugins.Manager, backendPM backendplugin.Manager, settingsProvider setting.Provider,
|
||||
dataService *tsdb.Service, alertEngine *alerting.AlertEngine,
|
||||
pluginRequestValidator models.PluginRequestValidator, pluginStaticRouteResolver plugins.StaticRouteResolver,
|
||||
pluginDashboardManager plugins.PluginDashboardManager, pluginStore plugins.Store, pluginClient plugins.Client,
|
||||
pluginErrorResolver plugins.ErrorResolver, settingsProvider setting.Provider,
|
||||
dataSourceCache datasources.CacheService, userTokenService models.UserTokenService,
|
||||
cleanUpService *cleanup.CleanUpService, shortURLService shorturls.Service,
|
||||
remoteCache *remotecache.RemoteCache, provisioningService provisioning.ProvisioningService,
|
||||
@ -134,56 +138,60 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
notificationService *notifications.NotificationService, tracingService *tracing.TracingService,
|
||||
internalMetricsSvc *metrics.InternalMetricsService, quotaService *quota.QuotaService,
|
||||
socialService social.Service, oauthTokenService oauthtoken.OAuthTokenService,
|
||||
encryptionService encryption.Service, searchUsersService searchusers.Service,
|
||||
encryptionService encryption.Service, updateChecker *updatechecker.Service, searchUsersService searchusers.Service,
|
||||
dataSourcesService *datasources.Service) (*HTTPServer, error) {
|
||||
web.Env = cfg.Env
|
||||
m := web.New()
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: cfg,
|
||||
RouteRegister: routeRegister,
|
||||
Bus: bus,
|
||||
RenderService: renderService,
|
||||
License: licensing,
|
||||
HooksService: hooksService,
|
||||
CacheService: cacheService,
|
||||
SQLStore: sqlStore,
|
||||
DataService: dataService,
|
||||
AlertEngine: alertEngine,
|
||||
PluginRequestValidator: pluginRequestValidator,
|
||||
PluginManager: pluginManager,
|
||||
BackendPluginManager: backendPM,
|
||||
SettingsProvider: settingsProvider,
|
||||
DataSourceCache: dataSourceCache,
|
||||
AuthTokenService: userTokenService,
|
||||
cleanUpService: cleanUpService,
|
||||
ShortURLService: shortURLService,
|
||||
RemoteCacheService: remoteCache,
|
||||
ProvisioningService: provisioningService,
|
||||
Login: loginService,
|
||||
AccessControl: accessControl,
|
||||
DataProxy: dataSourceProxy,
|
||||
SearchService: searchService,
|
||||
Live: live,
|
||||
LivePushGateway: livePushGateway,
|
||||
PluginContextProvider: plugCtxProvider,
|
||||
ContextHandler: contextHandler,
|
||||
LoadSchemaService: schemaService,
|
||||
AlertNG: alertNG,
|
||||
LibraryPanelService: libraryPanelService,
|
||||
LibraryElementService: libraryElementService,
|
||||
QuotaService: quotaService,
|
||||
notificationService: notificationService,
|
||||
tracingService: tracingService,
|
||||
internalMetricsSvc: internalMetricsSvc,
|
||||
log: log.New("http.server"),
|
||||
web: m,
|
||||
Listener: opts.Listener,
|
||||
SocialService: socialService,
|
||||
OAuthTokenService: oauthTokenService,
|
||||
EncryptionService: encryptionService,
|
||||
DataSourcesService: dataSourcesService,
|
||||
searchUsersService: searchUsersService,
|
||||
Cfg: cfg,
|
||||
RouteRegister: routeRegister,
|
||||
Bus: bus,
|
||||
RenderService: renderService,
|
||||
License: licensing,
|
||||
HooksService: hooksService,
|
||||
CacheService: cacheService,
|
||||
SQLStore: sqlStore,
|
||||
DataService: dataService,
|
||||
AlertEngine: alertEngine,
|
||||
PluginRequestValidator: pluginRequestValidator,
|
||||
pluginClient: pluginClient,
|
||||
pluginStore: pluginStore,
|
||||
pluginStaticRouteResolver: pluginStaticRouteResolver,
|
||||
pluginDashboardManager: pluginDashboardManager,
|
||||
pluginErrorResolver: pluginErrorResolver,
|
||||
updateChecker: updateChecker,
|
||||
SettingsProvider: settingsProvider,
|
||||
DataSourceCache: dataSourceCache,
|
||||
AuthTokenService: userTokenService,
|
||||
cleanUpService: cleanUpService,
|
||||
ShortURLService: shortURLService,
|
||||
RemoteCacheService: remoteCache,
|
||||
ProvisioningService: provisioningService,
|
||||
Login: loginService,
|
||||
AccessControl: accessControl,
|
||||
DataProxy: dataSourceProxy,
|
||||
SearchService: searchService,
|
||||
Live: live,
|
||||
LivePushGateway: livePushGateway,
|
||||
PluginContextProvider: plugCtxProvider,
|
||||
ContextHandler: contextHandler,
|
||||
LoadSchemaService: schemaService,
|
||||
AlertNG: alertNG,
|
||||
LibraryPanelService: libraryPanelService,
|
||||
LibraryElementService: libraryElementService,
|
||||
QuotaService: quotaService,
|
||||
notificationService: notificationService,
|
||||
tracingService: tracingService,
|
||||
internalMetricsSvc: internalMetricsSvc,
|
||||
log: log.New("http.server"),
|
||||
web: m,
|
||||
Listener: opts.Listener,
|
||||
SocialService: socialService,
|
||||
OAuthTokenService: oauthTokenService,
|
||||
EncryptionService: encryptionService,
|
||||
DataSourcesService: dataSourcesService,
|
||||
searchUsersService: searchUsersService,
|
||||
}
|
||||
if hs.Listener != nil {
|
||||
hs.log.Debug("Using provided listener")
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/navlinks"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@ -65,21 +66,21 @@ func (hs *HTTPServer) getProfileNode(c *models.ReqContext) *dtos.NavLink {
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error) {
|
||||
enabledPlugins, err := hs.PluginManager.GetEnabledPlugins(c.OrgId)
|
||||
enabledPlugins, err := hs.enabledPlugins(c.OrgId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appLinks := []*dtos.NavLink{}
|
||||
for _, plugin := range enabledPlugins.Apps {
|
||||
for _, plugin := range enabledPlugins[plugins.App] {
|
||||
if !plugin.Pinned {
|
||||
continue
|
||||
}
|
||||
|
||||
appLink := &dtos.NavLink{
|
||||
Text: plugin.Name,
|
||||
Id: "plugin-page-" + plugin.Id,
|
||||
Url: plugin.DefaultNavUrl,
|
||||
Id: "plugin-page-" + plugin.ID,
|
||||
Url: plugin.DefaultNavURL,
|
||||
Img: plugin.Info.Logos.Small,
|
||||
SortWeight: dtos.WeightPlugin,
|
||||
}
|
||||
@ -101,7 +102,7 @@ func (hs *HTTPServer) getAppLinks(c *models.ReqContext) ([]*dtos.NavLink, error)
|
||||
}
|
||||
} else {
|
||||
link = &dtos.NavLink{
|
||||
Url: hs.Cfg.AppSubURL + "/plugins/" + plugin.Id + "/page/" + include.Slug,
|
||||
Url: hs.Cfg.AppSubURL + "/plugins/" + plugin.ID + "/page/" + include.Slug,
|
||||
Text: include.Name,
|
||||
}
|
||||
}
|
||||
@ -504,8 +505,8 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
|
||||
GoogleTagManagerId: setting.GoogleTagManagerId,
|
||||
BuildVersion: setting.BuildVersion,
|
||||
BuildCommit: setting.BuildCommit,
|
||||
NewGrafanaVersion: hs.PluginManager.GrafanaLatestVersion(),
|
||||
NewGrafanaVersionExists: hs.PluginManager.GrafanaHasUpdate(),
|
||||
NewGrafanaVersion: hs.updateChecker.LatestGrafanaVersion(),
|
||||
NewGrafanaVersionExists: hs.updateChecker.GrafanaUpdateAvailable(),
|
||||
AppName: setting.ApplicationName,
|
||||
AppNameBodyClass: getAppNameBodyClass(hs.License.HasValidLicense()),
|
||||
FavIcon: "public/img/fav32.png",
|
||||
|
@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/grafanads"
|
||||
|
||||
@ -12,6 +13,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/expr"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/adapters"
|
||||
)
|
||||
|
||||
// QueryMetricsV2 returns query metrics.
|
||||
@ -82,19 +84,17 @@ func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext, reqDTO dtos.MetricReq
|
||||
return response.Error(http.StatusForbidden, "Access denied", err)
|
||||
}
|
||||
|
||||
resp, err := hs.DataService.HandleRequest(c.Req.Context(), ds, request)
|
||||
req, err := hs.createRequest(ds, request)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Request formation error", err)
|
||||
}
|
||||
|
||||
resp, err := hs.pluginClient.QueryData(c.Req.Context(), req)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Metric request error", err)
|
||||
}
|
||||
|
||||
// This is insanity... but ¯\_(ツ)_/¯, the current query path looks like:
|
||||
// encodeJson( decodeBase64( encodeBase64( decodeArrow( encodeArrow(frame)) ) )
|
||||
// this will soon change to a more direct route
|
||||
qdr, err := resp.ToBackendDataResponse()
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "error converting results", err)
|
||||
}
|
||||
return toMacronResponse(qdr)
|
||||
return toMacronResponse(resp)
|
||||
}
|
||||
|
||||
func toMacronResponse(qdr *backend.QueryDataResponse) response.Response {
|
||||
@ -222,3 +222,42 @@ func (hs *HTTPServer) QueryMetrics(c *models.ReqContext, reqDto dtos.MetricReque
|
||||
|
||||
return response.JSON(statusCode, &resp)
|
||||
}
|
||||
|
||||
// nolint:staticcheck // plugins.DataQueryResponse deprecated
|
||||
func (hs *HTTPServer) createRequest(ds *models.DataSource, query plugins.DataQuery) (*backend.QueryDataRequest, error) {
|
||||
instanceSettings, err := adapters.ModelToInstanceSettings(ds, hs.decryptSecureJsonDataFn())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &backend.QueryDataRequest{
|
||||
PluginContext: backend.PluginContext{
|
||||
OrgID: ds.OrgId,
|
||||
PluginID: ds.Type,
|
||||
User: adapters.BackendUserFromSignedInUser(query.User),
|
||||
DataSourceInstanceSettings: instanceSettings,
|
||||
},
|
||||
Queries: []backend.DataQuery{},
|
||||
Headers: query.Headers,
|
||||
}
|
||||
|
||||
for _, q := range query.Queries {
|
||||
modelJSON, err := q.Model.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Queries = append(req.Queries, backend.DataQuery{
|
||||
RefID: q.RefID,
|
||||
Interval: time.Duration(q.IntervalMS) * time.Millisecond,
|
||||
MaxDataPoints: q.MaxDataPoints,
|
||||
TimeRange: backend.TimeRange{
|
||||
From: query.TimeRange.GetFromAsTimeUTC(),
|
||||
To: query.TimeRange.GetToAsTimeUTC(),
|
||||
},
|
||||
QueryType: q.QueryType,
|
||||
JSON: modelJSON,
|
||||
})
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ type DSInfo struct {
|
||||
}
|
||||
|
||||
// ApplyRoute should use the plugin route data to set auth headers and custom headers.
|
||||
func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route *plugins.AppPluginRoute,
|
||||
func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route *plugins.Route,
|
||||
ds DSInfo, cfg *setting.Cfg) {
|
||||
proxyPath = strings.TrimPrefix(proxyPath, route.Path)
|
||||
|
||||
@ -76,7 +76,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
|
||||
}
|
||||
}
|
||||
|
||||
func getTokenProvider(ctx context.Context, cfg *setting.Cfg, ds DSInfo, pluginRoute *plugins.AppPluginRoute,
|
||||
func getTokenProvider(ctx context.Context, cfg *setting.Cfg, ds DSInfo, pluginRoute *plugins.Route,
|
||||
data templateData) (accessTokenProvider, error) {
|
||||
authType := pluginRoute.AuthType
|
||||
|
||||
@ -133,7 +133,7 @@ func getTokenProvider(ctx context.Context, cfg *setting.Cfg, ds DSInfo, pluginRo
|
||||
}
|
||||
}
|
||||
|
||||
func interpolateAuthParams(tokenAuth *plugins.JwtTokenAuth, data templateData) (*plugins.JwtTokenAuth, error) {
|
||||
func interpolateAuthParams(tokenAuth *plugins.JWTTokenAuth, data templateData) (*plugins.JWTTokenAuth, error) {
|
||||
if tokenAuth == nil {
|
||||
// Nothing to interpolate
|
||||
return nil, nil
|
||||
@ -153,7 +153,7 @@ func interpolateAuthParams(tokenAuth *plugins.JwtTokenAuth, data templateData) (
|
||||
interpolatedParams[key] = interpolatedParam
|
||||
}
|
||||
|
||||
return &plugins.JwtTokenAuth{
|
||||
return &plugins.JWTTokenAuth{
|
||||
Url: interpolatedUrl,
|
||||
Scopes: tokenAuth.Scopes,
|
||||
Params: interpolatedParams,
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func TestApplyRoute_interpolateAuthParams(t *testing.T) {
|
||||
tokenAuth := &plugins.JwtTokenAuth{
|
||||
tokenAuth := &plugins.JWTTokenAuth{
|
||||
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
|
||||
Scopes: []string{
|
||||
"https://www.testapi.com/auth/Read.All",
|
||||
@ -38,7 +38,7 @@ func TestApplyRoute_interpolateAuthParams(t *testing.T) {
|
||||
SecureJsonData: map[string]string{},
|
||||
}
|
||||
|
||||
t.Run("should interpolate JwtTokenAuth struct using given JsonData", func(t *testing.T) {
|
||||
t.Run("should interpolate JWTTokenAuth struct using given JsonData", func(t *testing.T) {
|
||||
interpolated, err := interpolateAuthParams(tokenAuth, validData)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, interpolated)
|
||||
@ -54,7 +54,7 @@ func TestApplyRoute_interpolateAuthParams(t *testing.T) {
|
||||
assert.Equal(t, "testkey", interpolated.Params["private_key"])
|
||||
})
|
||||
|
||||
t.Run("should return Nil if given JwtTokenAuth is Nil", func(t *testing.T) {
|
||||
t.Run("should return Nil if given JWTTokenAuth is Nil", func(t *testing.T) {
|
||||
interpolated, err := interpolateAuthParams(nil, validData)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, interpolated)
|
||||
|
@ -36,8 +36,8 @@ type DataSourceProxy struct {
|
||||
ctx *models.ReqContext
|
||||
targetUrl *url.URL
|
||||
proxyPath string
|
||||
route *plugins.AppPluginRoute
|
||||
plugin *plugins.DataSourcePlugin
|
||||
matchedRoute *plugins.Route
|
||||
pluginRoutes []*plugins.Route
|
||||
cfg *setting.Cfg
|
||||
clientProvider httpclient.Provider
|
||||
oAuthTokenService oauthtoken.OAuthTokenService
|
||||
@ -73,7 +73,7 @@ func (lw *logWrapper) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
// NewDataSourceProxy creates a new Datasource proxy
|
||||
func NewDataSourceProxy(ds *models.DataSource, plugin *plugins.DataSourcePlugin, ctx *models.ReqContext,
|
||||
func NewDataSourceProxy(ds *models.DataSource, pluginRoutes []*plugins.Route, ctx *models.ReqContext,
|
||||
proxyPath string, cfg *setting.Cfg, clientProvider httpclient.Provider,
|
||||
oAuthTokenService oauthtoken.OAuthTokenService, dsService *datasources.Service) (*DataSourceProxy, error) {
|
||||
targetURL, err := datasource.ValidateURL(ds.Type, ds.Url)
|
||||
@ -83,7 +83,7 @@ func NewDataSourceProxy(ds *models.DataSource, plugin *plugins.DataSourcePlugin,
|
||||
|
||||
return &DataSourceProxy{
|
||||
ds: ds,
|
||||
plugin: plugin,
|
||||
pluginRoutes: pluginRoutes,
|
||||
ctx: ctx,
|
||||
proxyPath: proxyPath,
|
||||
targetUrl: targetURL,
|
||||
@ -257,8 +257,8 @@ func (proxy *DataSourceProxy) director(req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if proxy.route != nil {
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, DSInfo{
|
||||
if proxy.matchedRoute != nil {
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, DSInfo{
|
||||
ID: proxy.ds.Id,
|
||||
Updated: proxy.ds.Updated,
|
||||
JSONData: jsonData,
|
||||
@ -291,27 +291,25 @@ func (proxy *DataSourceProxy) validateRequest() error {
|
||||
}
|
||||
|
||||
// found route if there are any
|
||||
if len(proxy.plugin.Routes) > 0 {
|
||||
for _, route := range proxy.plugin.Routes {
|
||||
// method match
|
||||
if route.Method != "" && route.Method != "*" && route.Method != proxy.ctx.Req.Method {
|
||||
continue
|
||||
}
|
||||
|
||||
// route match
|
||||
if !strings.HasPrefix(proxy.proxyPath, route.Path) {
|
||||
continue
|
||||
}
|
||||
|
||||
if route.ReqRole.IsValid() {
|
||||
if !proxy.ctx.HasUserRole(route.ReqRole) {
|
||||
return errors.New("plugin proxy route access denied")
|
||||
}
|
||||
}
|
||||
|
||||
proxy.route = route
|
||||
return nil
|
||||
for _, route := range proxy.pluginRoutes {
|
||||
// method match
|
||||
if route.Method != "" && route.Method != "*" && route.Method != proxy.ctx.Req.Method {
|
||||
continue
|
||||
}
|
||||
|
||||
// route match
|
||||
if !strings.HasPrefix(proxy.proxyPath, route.Path) {
|
||||
continue
|
||||
}
|
||||
|
||||
if route.ReqRole.IsValid() {
|
||||
if !proxy.ctx.HasUserRole(route.ReqRole) {
|
||||
return errors.New("plugin proxy route access denied")
|
||||
}
|
||||
}
|
||||
|
||||
proxy.matchedRoute = route
|
||||
return nil
|
||||
}
|
||||
|
||||
// Trailing validation below this point for routes that were not matched
|
||||
|
@ -32,51 +32,49 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
httpClientProvider := httpclient.NewProvider()
|
||||
|
||||
t.Run("Plugin with routes", func(t *testing.T) {
|
||||
plugin := &plugins.DataSourcePlugin{
|
||||
Routes: []*plugins.AppPluginRoute{
|
||||
{
|
||||
Path: "api/v4/",
|
||||
URL: "https://www.google.com",
|
||||
ReqRole: models.ROLE_EDITOR,
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
routes := []*plugins.Route{
|
||||
{
|
||||
Path: "api/v4/",
|
||||
URL: "https://www.google.com",
|
||||
ReqRole: models.ROLE_EDITOR,
|
||||
Headers: []plugins.Header{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
{
|
||||
Path: "api/admin",
|
||||
URL: "https://www.google.com",
|
||||
ReqRole: models.ROLE_ADMIN,
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "api/admin",
|
||||
URL: "https://www.google.com",
|
||||
ReqRole: models.ROLE_ADMIN,
|
||||
Headers: []plugins.Header{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
{
|
||||
Path: "api/anon",
|
||||
URL: "https://www.google.com",
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "api/anon",
|
||||
URL: "https://www.google.com",
|
||||
Headers: []plugins.Header{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
{
|
||||
Path: "api/common",
|
||||
URL: "{{.JsonData.dynamicUrl}}",
|
||||
URLParams: []plugins.AppPluginRouteURLParam{
|
||||
{Name: "{{.JsonData.queryParam}}", Content: "{{.SecureJsonData.key}}"},
|
||||
},
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "api/common",
|
||||
URL: "{{.JsonData.dynamicUrl}}",
|
||||
URLParams: []plugins.URLParam{
|
||||
{Name: "{{.JsonData.queryParam}}", Content: "{{.SecureJsonData.key}}"},
|
||||
},
|
||||
{
|
||||
Path: "api/restricted",
|
||||
ReqRole: models.ROLE_ADMIN,
|
||||
},
|
||||
{
|
||||
Path: "api/body",
|
||||
URL: "http://www.test.com",
|
||||
Body: []byte(`{ "url": "{{.JsonData.dynamicUrl}}", "secret": "{{.SecureJsonData.key}}" }`),
|
||||
Headers: []plugins.Header{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "api/restricted",
|
||||
ReqRole: models.ROLE_ADMIN,
|
||||
},
|
||||
{
|
||||
Path: "api/body",
|
||||
URL: "http://www.test.com",
|
||||
Body: []byte(`{ "url": "{{.JsonData.dynamicUrl}}", "secret": "{{.SecureJsonData.key}}" }`),
|
||||
},
|
||||
}
|
||||
|
||||
origSecretKey := setting.SecretKey
|
||||
@ -125,10 +123,11 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
t.Run("When matching route path", func(t *testing.T) {
|
||||
ctx, req := setUp()
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider,
|
||||
&oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
proxy.route = plugin.Routes[0]
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, dsInfo, cfg)
|
||||
proxy.matchedRoute = routes[0]
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, cfg)
|
||||
|
||||
assert.Equal(t, "https://www.google.com/some/method", req.URL.String())
|
||||
assert.Equal(t, "my secret 123", req.Header.Get("x-header"))
|
||||
@ -137,10 +136,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
t.Run("When matching route path and has dynamic url", func(t *testing.T) {
|
||||
ctx, req := setUp()
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/common/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
proxy.route = plugin.Routes[3]
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, dsInfo, cfg)
|
||||
proxy.matchedRoute = routes[3]
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, cfg)
|
||||
|
||||
assert.Equal(t, "https://dynamic.grafana.com/some/method?apiKey=123", req.URL.String())
|
||||
assert.Equal(t, "my secret 123", req.Header.Get("x-header"))
|
||||
@ -149,10 +148,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
t.Run("When matching route path with no url", func(t *testing.T) {
|
||||
ctx, req := setUp()
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
proxy.route = plugin.Routes[4]
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, dsInfo, cfg)
|
||||
proxy.matchedRoute = routes[4]
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, cfg)
|
||||
|
||||
assert.Equal(t, "http://localhost/asd", req.URL.String())
|
||||
})
|
||||
@ -160,10 +159,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
t.Run("When matching route path and has dynamic body", func(t *testing.T) {
|
||||
ctx, req := setUp()
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/body", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/body", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
proxy.route = plugin.Routes[5]
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, dsInfo, cfg)
|
||||
proxy.matchedRoute = routes[5]
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, cfg)
|
||||
|
||||
content, err := ioutil.ReadAll(req.Body)
|
||||
require.NoError(t, err)
|
||||
@ -174,7 +173,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
t.Run("plugin route with valid role", func(t *testing.T) {
|
||||
ctx, _ := setUp()
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
err = proxy.validateRequest()
|
||||
require.NoError(t, err)
|
||||
@ -183,7 +182,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
t.Run("plugin route with admin role and user is editor", func(t *testing.T) {
|
||||
ctx, _ := setUp()
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
err = proxy.validateRequest()
|
||||
require.Error(t, err)
|
||||
@ -193,7 +192,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
ctx, _ := setUp()
|
||||
ctx.SignedInUser.OrgRole = models.ROLE_ADMIN
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
err = proxy.validateRequest()
|
||||
require.NoError(t, err)
|
||||
@ -202,32 +201,30 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Plugin with multiple routes for token auth", func(t *testing.T) {
|
||||
plugin := &plugins.DataSourcePlugin{
|
||||
Routes: []*plugins.AppPluginRoute{
|
||||
{
|
||||
Path: "pathwithtoken1",
|
||||
URL: "https://api.nr1.io/some/path",
|
||||
TokenAuth: &plugins.JwtTokenAuth{
|
||||
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
|
||||
Params: map[string]string{
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": "{{.JsonData.clientId}}",
|
||||
"client_secret": "{{.SecureJsonData.clientSecret}}",
|
||||
"resource": "https://api.nr1.io",
|
||||
},
|
||||
routes := []*plugins.Route{
|
||||
{
|
||||
Path: "pathwithtoken1",
|
||||
URL: "https://api.nr1.io/some/path",
|
||||
TokenAuth: &plugins.JWTTokenAuth{
|
||||
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
|
||||
Params: map[string]string{
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": "{{.JsonData.clientId}}",
|
||||
"client_secret": "{{.SecureJsonData.clientSecret}}",
|
||||
"resource": "https://api.nr1.io",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "pathwithtoken2",
|
||||
URL: "https://api.nr2.io/some/path",
|
||||
TokenAuth: &plugins.JwtTokenAuth{
|
||||
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
|
||||
Params: map[string]string{
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": "{{.JsonData.clientId}}",
|
||||
"client_secret": "{{.SecureJsonData.clientSecret}}",
|
||||
"resource": "https://api.nr2.io",
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "pathwithtoken2",
|
||||
URL: "https://api.nr2.io/some/path",
|
||||
TokenAuth: &plugins.JWTTokenAuth{
|
||||
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
|
||||
Params: map[string]string{
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": "{{.JsonData.clientId}}",
|
||||
"client_secret": "{{.SecureJsonData.clientSecret}}",
|
||||
"resource": "https://api.nr2.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -285,9 +282,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
}
|
||||
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, plugin.Routes[0], dsInfo, cfg)
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
|
||||
|
||||
authorizationHeaderCall1 = req.Header.Get("Authorization")
|
||||
assert.Equal(t, "https://api.nr1.io/some/path", req.URL.String())
|
||||
@ -301,9 +298,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
client = newFakeHTTPClient(t, json2)
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken2", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, plugin.Routes[1], dsInfo, cfg)
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[1], dsInfo, cfg)
|
||||
|
||||
authorizationHeaderCall2 = req.Header.Get("Authorization")
|
||||
|
||||
@ -318,9 +315,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
|
||||
client = newFakeHTTPClient(t, []byte{})
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, plugin.Routes[0], dsInfo, cfg)
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
|
||||
|
||||
authorizationHeaderCall3 := req.Header.Get("Authorization")
|
||||
assert.Equal(t, "https://api.nr1.io/some/path", req.URL.String())
|
||||
@ -334,12 +331,12 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When proxying graphite", func(t *testing.T) {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
var routes []*plugins.Route
|
||||
ds := &models.DataSource{Url: "htttp://graphite:8080", Type: models.DS_GRAPHITE}
|
||||
ctx := &models.ReqContext{}
|
||||
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{BuildVersion: "5.3.0"}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{BuildVersion: "5.3.0"}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
require.NoError(t, err)
|
||||
@ -353,8 +350,6 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When proxying InfluxDB", func(t *testing.T) {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
|
||||
ds := &models.DataSource{
|
||||
Type: models.DS_INFLUXDB_08,
|
||||
Url: "http://influxdb:8083",
|
||||
@ -364,8 +359,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
}
|
||||
|
||||
ctx := &models.ReqContext{}
|
||||
var routes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
@ -376,8 +372,6 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When proxying a data source with no keepCookies specified", func(t *testing.T) {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
|
||||
json, err := simplejson.NewJson([]byte(`{"keepCookies": []}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -388,8 +382,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
}
|
||||
|
||||
ctx := &models.ReqContext{}
|
||||
var routes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
requestURL, err := url.Parse("http://grafana.com/sub")
|
||||
@ -404,8 +399,6 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When proxying a data source with keep cookies specified", func(t *testing.T) {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
|
||||
json, err := simplejson.NewJson([]byte(`{"keepCookies": ["JSESSION_ID"]}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -416,8 +409,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
}
|
||||
|
||||
ctx := &models.ReqContext{}
|
||||
var pluginRoutes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, pluginRoutes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
requestURL, err := url.Parse("http://grafana.com/sub")
|
||||
@ -432,14 +426,14 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When proxying a custom datasource", func(t *testing.T) {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
ds := &models.DataSource{
|
||||
Type: "custom-datasource",
|
||||
Url: "http://host/root/",
|
||||
}
|
||||
ctx := &models.ReqContext{}
|
||||
var routes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
req.Header.Set("Origin", "grafana.com")
|
||||
@ -470,7 +464,6 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
ds := &models.DataSource{
|
||||
Type: "custom-datasource",
|
||||
Url: "http://host/root/",
|
||||
@ -494,8 +487,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
},
|
||||
oAuthEnabled: true,
|
||||
}
|
||||
var routes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &mockAuthToken, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &mockAuthToken, dsService)
|
||||
require.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
require.NoError(t, err)
|
||||
@ -570,8 +564,6 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
httpClientProvider := httpclient.NewProvider()
|
||||
var writeErr error
|
||||
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
|
||||
type setUpCfg struct {
|
||||
headers map[string]string
|
||||
writeCb func(w http.ResponseWriter, r *http.Request)
|
||||
@ -621,8 +613,10 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
|
||||
t.Run("When response header Set-Cookie is not set should remove proxied Set-Cookie header", func(t *testing.T) {
|
||||
ctx, ds := setUp(t)
|
||||
|
||||
var routes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.HandleRequest()
|
||||
@ -637,8 +631,9 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
"Set-Cookie": "important_cookie=important_value",
|
||||
},
|
||||
})
|
||||
var routes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.HandleRequest()
|
||||
@ -657,8 +652,9 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
t.Log("Wrote 401 response")
|
||||
},
|
||||
})
|
||||
var routes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.HandleRequest()
|
||||
@ -680,8 +676,9 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
})
|
||||
|
||||
ctx.Req = httptest.NewRequest("GET", "/api/datasources/proxy/1/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", nil)
|
||||
var routes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.HandleRequest()
|
||||
@ -702,9 +699,9 @@ func TestNewDataSourceProxy_InvalidURL(t *testing.T) {
|
||||
Url: "://host/root",
|
||||
}
|
||||
cfg := setting.Cfg{}
|
||||
plugin := plugins.DataSourcePlugin{}
|
||||
var routes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
_, err := NewDataSourceProxy(&ds, &plugin, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService)
|
||||
_, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService)
|
||||
require.Error(t, err)
|
||||
assert.True(t, strings.HasPrefix(err.Error(), `validation of data source URL "://host/root" failed`))
|
||||
}
|
||||
@ -719,10 +716,10 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) {
|
||||
Url: "127.0.01:5432",
|
||||
}
|
||||
cfg := setting.Cfg{}
|
||||
plugin := plugins.DataSourcePlugin{}
|
||||
|
||||
var routes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
_, err := NewDataSourceProxy(&ds, &plugin, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService)
|
||||
_, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService)
|
||||
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@ -754,14 +751,14 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) {
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
cfg := setting.Cfg{}
|
||||
plugin := plugins.DataSourcePlugin{}
|
||||
ds := models.DataSource{
|
||||
Type: "mssql",
|
||||
Url: tc.url,
|
||||
}
|
||||
|
||||
var routes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
p, err := NewDataSourceProxy(&ds, &plugin, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService)
|
||||
p, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", &cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService)
|
||||
if tc.err == nil {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &url.URL{
|
||||
@ -778,15 +775,14 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) {
|
||||
|
||||
// getDatasourceProxiedRequest is a helper for easier setup of tests based on global config and ReqContext.
|
||||
func getDatasourceProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *setting.Cfg) *http.Request {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
|
||||
ds := &models.DataSource{
|
||||
Type: "custom",
|
||||
Url: "http://host/root/",
|
||||
}
|
||||
|
||||
var routes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
require.NoError(t, err)
|
||||
@ -903,10 +899,10 @@ func createAuthTest(t *testing.T, dsType string, authType string, authCheck stri
|
||||
}
|
||||
|
||||
func runDatasourceAuthTest(t *testing.T, test *testCase) {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
ctx := &models.ReqContext{}
|
||||
var routes []*plugins.Route
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(test.datasource, plugin, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
@ -919,22 +915,21 @@ func runDatasourceAuthTest(t *testing.T, test *testCase) {
|
||||
|
||||
func Test_PathCheck(t *testing.T) {
|
||||
// Ensure that we test routes appropriately. This test reproduces a historical bug where two routes were defined with different role requirements but the same method and the more privileged route was tested first. Here we ensure auth checks are applied based on the correct route, not just the method.
|
||||
plugin := &plugins.DataSourcePlugin{
|
||||
Routes: []*plugins.AppPluginRoute{
|
||||
{
|
||||
Path: "a",
|
||||
URL: "https://www.google.com",
|
||||
ReqRole: models.ROLE_EDITOR,
|
||||
Method: http.MethodGet,
|
||||
},
|
||||
{
|
||||
Path: "b",
|
||||
URL: "https://www.google.com",
|
||||
ReqRole: models.ROLE_VIEWER,
|
||||
Method: http.MethodGet,
|
||||
},
|
||||
routes := []*plugins.Route{
|
||||
{
|
||||
Path: "a",
|
||||
URL: "https://www.google.com",
|
||||
ReqRole: models.ROLE_EDITOR,
|
||||
Method: http.MethodGet,
|
||||
},
|
||||
{
|
||||
Path: "b",
|
||||
URL: "https://www.google.com",
|
||||
ReqRole: models.ROLE_VIEWER,
|
||||
Method: http.MethodGet,
|
||||
},
|
||||
}
|
||||
|
||||
setUp := func() (*models.ReqContext, *http.Request) {
|
||||
req, err := http.NewRequest("GET", "http://localhost/asd", nil)
|
||||
require.NoError(t, err)
|
||||
@ -946,11 +941,11 @@ func Test_PathCheck(t *testing.T) {
|
||||
}
|
||||
ctx, _ := setUp()
|
||||
dsService := datasources.ProvideService(bus.New(), nil, ossencryption.ProvideService())
|
||||
proxy, err := NewDataSourceProxy(&models.DataSource{}, plugin, ctx, "b", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService)
|
||||
proxy, err := NewDataSourceProxy(&models.DataSource{}, routes, ctx, "b", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Nil(t, proxy.validateRequest())
|
||||
require.Equal(t, plugin.Routes[1], proxy.route)
|
||||
require.Equal(t, routes[1], proxy.matchedRoute)
|
||||
}
|
||||
|
||||
type mockOAuthTokenService struct {
|
||||
|
@ -21,7 +21,7 @@ type templateData struct {
|
||||
}
|
||||
|
||||
// NewApiPluginProxy create a plugin proxy
|
||||
func NewApiPluginProxy(ctx *models.ReqContext, proxyPath string, route *plugins.AppPluginRoute,
|
||||
func NewApiPluginProxy(ctx *models.ReqContext, proxyPath string, route *plugins.Route,
|
||||
appID string, cfg *setting.Cfg, encryptionService encryption.Service) *httputil.ReverseProxy {
|
||||
director := func(req *http.Request) {
|
||||
query := models.GetPluginSettingByIdQuery{OrgId: ctx.OrgId, PluginId: appID}
|
||||
|
@ -18,8 +18,8 @@ import (
|
||||
|
||||
func TestPluginProxy(t *testing.T) {
|
||||
t.Run("When getting proxy headers", func(t *testing.T) {
|
||||
route := &plugins.AppPluginRoute{
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
route := &plugins.Route{
|
||||
Headers: []plugins.Header{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
}
|
||||
@ -124,7 +124,7 @@ func TestPluginProxy(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When getting templated url", func(t *testing.T) {
|
||||
route := &plugins.AppPluginRoute{
|
||||
route := &plugins.Route{
|
||||
URL: "{{.JsonData.dynamicUrl}}",
|
||||
Method: "GET",
|
||||
}
|
||||
@ -159,7 +159,7 @@ func TestPluginProxy(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When getting complex templated url", func(t *testing.T) {
|
||||
route := &plugins.AppPluginRoute{
|
||||
route := &plugins.Route{
|
||||
URL: "{{if .JsonData.apiHost}}{{.JsonData.apiHost}}{{else}}https://example.com{{end}}",
|
||||
Method: "GET",
|
||||
}
|
||||
@ -189,7 +189,7 @@ func TestPluginProxy(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When getting templated body", func(t *testing.T) {
|
||||
route := &plugins.AppPluginRoute{
|
||||
route := &plugins.Route{
|
||||
Path: "api/body",
|
||||
URL: "http://www.test.com",
|
||||
Body: []byte(`{ "url": "{{.JsonData.dynamicUrl}}", "secret": "{{.SecureJsonData.key}}" }`),
|
||||
@ -238,10 +238,10 @@ func TestPluginProxy(t *testing.T) {
|
||||
}
|
||||
|
||||
// getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext.
|
||||
func getPluginProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *setting.Cfg, route *plugins.AppPluginRoute) *http.Request {
|
||||
func getPluginProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *setting.Cfg, route *plugins.Route) *http.Request {
|
||||
// insert dummy route if none is specified
|
||||
if route == nil {
|
||||
route = &plugins.AppPluginRoute{
|
||||
route = &plugins.Route{
|
||||
Path: "api/v4/",
|
||||
URL: "https://www.google.com",
|
||||
ReqRole: models.ROLE_EDITOR,
|
||||
|
@ -16,7 +16,7 @@ type azureAccessTokenProvider struct {
|
||||
scopes []string
|
||||
}
|
||||
|
||||
func newAzureAccessTokenProvider(ctx context.Context, cfg *setting.Cfg, authParams *plugins.JwtTokenAuth) (*azureAccessTokenProvider, error) {
|
||||
func newAzureAccessTokenProvider(ctx context.Context, cfg *setting.Cfg, authParams *plugins.JWTTokenAuth) (*azureAccessTokenProvider, error) {
|
||||
credentials := getAzureCredentials(cfg, authParams)
|
||||
tokenProvider, err := aztokenprovider.NewAzureAccessTokenProvider(cfg, credentials)
|
||||
if err != nil {
|
||||
@ -33,7 +33,7 @@ func (provider *azureAccessTokenProvider) GetAccessToken() (string, error) {
|
||||
return provider.tokenProvider.GetAccessToken(provider.ctx, provider.scopes)
|
||||
}
|
||||
|
||||
func getAzureCredentials(cfg *setting.Cfg, authParams *plugins.JwtTokenAuth) azcredentials.AzureCredentials {
|
||||
func getAzureCredentials(cfg *setting.Cfg, authParams *plugins.JWTTokenAuth) azcredentials.AzureCredentials {
|
||||
authType := strings.ToLower(authParams.Params["azure_auth_type"])
|
||||
clientId := authParams.Params["client_id"]
|
||||
|
||||
|
@ -12,8 +12,8 @@ type gceAccessTokenProvider struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func newGceAccessTokenProvider(ctx context.Context, ds DSInfo, pluginRoute *plugins.AppPluginRoute,
|
||||
authParams *plugins.JwtTokenAuth) *gceAccessTokenProvider {
|
||||
func newGceAccessTokenProvider(ctx context.Context, ds DSInfo, pluginRoute *plugins.Route,
|
||||
authParams *plugins.JWTTokenAuth) *gceAccessTokenProvider {
|
||||
cfg := googletokenprovider.Config{
|
||||
RoutePath: pluginRoute.Path,
|
||||
RouteMethod: pluginRoute.Method,
|
||||
|
@ -27,8 +27,8 @@ type tokenCacheType struct {
|
||||
type genericAccessTokenProvider struct {
|
||||
datasourceId int64
|
||||
datasourceUpdated time.Time
|
||||
route *plugins.AppPluginRoute
|
||||
authParams *plugins.JwtTokenAuth
|
||||
route *plugins.Route
|
||||
authParams *plugins.JWTTokenAuth
|
||||
}
|
||||
|
||||
type jwtToken struct {
|
||||
@ -67,8 +67,8 @@ func (token *jwtToken) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newGenericAccessTokenProvider(ds DSInfo, pluginRoute *plugins.AppPluginRoute,
|
||||
authParams *plugins.JwtTokenAuth) *genericAccessTokenProvider {
|
||||
func newGenericAccessTokenProvider(ds DSInfo, pluginRoute *plugins.Route,
|
||||
authParams *plugins.JWTTokenAuth) *genericAccessTokenProvider {
|
||||
return &genericAccessTokenProvider{
|
||||
datasourceId: ds.ID,
|
||||
datasourceUpdated: ds.Updated,
|
||||
|
@ -12,8 +12,8 @@ type jwtAccessTokenProvider struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func newJwtAccessTokenProvider(ctx context.Context, ds DSInfo, pluginRoute *plugins.AppPluginRoute,
|
||||
authParams *plugins.JwtTokenAuth) *jwtAccessTokenProvider {
|
||||
func newJwtAccessTokenProvider(ctx context.Context, ds DSInfo, pluginRoute *plugins.Route,
|
||||
authParams *plugins.JWTTokenAuth) *jwtAccessTokenProvider {
|
||||
jwtConf := &googletokenprovider.JwtTokenConfig{}
|
||||
if val, ok := authParams.Params["client_email"]; ok {
|
||||
jwtConf.Email = val
|
||||
|
@ -23,11 +23,11 @@ func TestAccessToken_pluginWithTokenAuthRoute(t *testing.T) {
|
||||
server := httptest.NewServer(apiHandler)
|
||||
defer server.Close()
|
||||
|
||||
pluginRoute := &plugins.AppPluginRoute{
|
||||
pluginRoute := &plugins.Route{
|
||||
Path: "pathwithtokenauth1",
|
||||
URL: "",
|
||||
Method: "GET",
|
||||
TokenAuth: &plugins.JwtTokenAuth{
|
||||
TokenAuth: &plugins.JWTTokenAuth{
|
||||
Url: server.URL + "/oauth/token",
|
||||
Scopes: []string{
|
||||
"https://www.testapi.com/auth/monitoring.read",
|
||||
@ -43,7 +43,7 @@ func TestAccessToken_pluginWithTokenAuthRoute(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
authParams := &plugins.JwtTokenAuth{
|
||||
authParams := &plugins.JWTTokenAuth{
|
||||
Url: server.URL + "/oauth/token",
|
||||
Scopes: []string{
|
||||
"https://www.testapi.com/auth/monitoring.read",
|
||||
|
@ -38,7 +38,7 @@ func interpolateString(text string, data templateData) (string, error) {
|
||||
}
|
||||
|
||||
// addHeaders interpolates route headers and injects them into the request headers
|
||||
func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
|
||||
func addHeaders(reqHeaders *http.Header, route *plugins.Route, data templateData) error {
|
||||
for _, header := range route.Headers {
|
||||
interpolated, err := interpolateString(header.Content, data)
|
||||
if err != nil {
|
||||
@ -51,7 +51,7 @@ func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data tem
|
||||
}
|
||||
|
||||
// addQueryString interpolates route params and injects them into the request object
|
||||
func addQueryString(req *http.Request, route *plugins.AppPluginRoute, data templateData) error {
|
||||
func addQueryString(req *http.Request, route *plugins.Route, data templateData) error {
|
||||
q := req.URL.Query()
|
||||
for _, param := range route.URLParams {
|
||||
interpolatedName, err := interpolateString(param.Name, data)
|
||||
@ -71,7 +71,7 @@ func addQueryString(req *http.Request, route *plugins.AppPluginRoute, data templ
|
||||
return nil
|
||||
}
|
||||
|
||||
func setBodyContent(req *http.Request, route *plugins.AppPluginRoute, data templateData) error {
|
||||
func setBodyContent(req *http.Request, route *plugins.Route, data templateData) error {
|
||||
if route.Body != nil {
|
||||
interpolatedBody, err := interpolateString(string(route.Body), data)
|
||||
if err != nil {
|
||||
|
@ -3,15 +3,19 @@ package api
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
@ -31,48 +35,48 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
|
||||
coreFilter = "1"
|
||||
}
|
||||
|
||||
pluginSettingsMap, err := hs.PluginManager.GetPluginSettings(c.OrgId)
|
||||
pluginSettingsMap, err := hs.pluginSettings(c.OrgId)
|
||||
if err != nil {
|
||||
return response.Error(500, "Failed to get list of plugins", err)
|
||||
}
|
||||
|
||||
result := make(dtos.PluginList, 0)
|
||||
for _, pluginDef := range hs.PluginManager.Plugins() {
|
||||
for _, pluginDef := range hs.pluginStore.Plugins() {
|
||||
// filter out app sub plugins
|
||||
if embeddedFilter == "0" && pluginDef.IncludedInAppId != "" {
|
||||
if embeddedFilter == "0" && pluginDef.IncludedInAppID != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// filter out core plugins
|
||||
if (coreFilter == "0" && pluginDef.IsCorePlugin) || (coreFilter == "1" && !pluginDef.IsCorePlugin) {
|
||||
if (coreFilter == "0" && pluginDef.IsCorePlugin()) || (coreFilter == "1" && !pluginDef.IsCorePlugin()) {
|
||||
continue
|
||||
}
|
||||
|
||||
// filter on type
|
||||
if typeFilter != "" && typeFilter != pluginDef.Type {
|
||||
if typeFilter != "" && typeFilter != string(pluginDef.Type) {
|
||||
continue
|
||||
}
|
||||
|
||||
if pluginDef.State == plugins.PluginStateAlpha && !hs.Cfg.PluginsEnableAlpha {
|
||||
if pluginDef.State == plugins.AlphaRelease && !hs.Cfg.PluginsEnableAlpha {
|
||||
continue
|
||||
}
|
||||
|
||||
listItem := dtos.PluginListItem{
|
||||
Id: pluginDef.Id,
|
||||
Id: pluginDef.ID,
|
||||
Name: pluginDef.Name,
|
||||
Type: pluginDef.Type,
|
||||
Type: string(pluginDef.Type),
|
||||
Category: pluginDef.Category,
|
||||
Info: &pluginDef.Info,
|
||||
LatestVersion: pluginDef.GrafanaNetVersion,
|
||||
HasUpdate: pluginDef.GrafanaNetHasUpdate,
|
||||
DefaultNavUrl: pluginDef.DefaultNavUrl,
|
||||
LatestVersion: pluginDef.GrafanaComVersion,
|
||||
HasUpdate: pluginDef.GrafanaComHasUpdate,
|
||||
DefaultNavUrl: pluginDef.DefaultNavURL,
|
||||
State: pluginDef.State,
|
||||
Signature: pluginDef.Signature,
|
||||
SignatureType: pluginDef.SignatureType,
|
||||
SignatureOrg: pluginDef.SignatureOrg,
|
||||
}
|
||||
|
||||
if pluginSetting, exists := pluginSettingsMap[pluginDef.Id]; exists {
|
||||
if pluginSetting, exists := pluginSettingsMap[pluginDef.ID]; exists {
|
||||
listItem.Enabled = pluginSetting.Enabled
|
||||
listItem.Pinned = pluginSetting.Pinned
|
||||
}
|
||||
@ -86,11 +90,9 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
|
||||
continue
|
||||
}
|
||||
|
||||
// filter out built in data sources
|
||||
if ds := hs.PluginManager.GetDataSource(pluginDef.Id); ds != nil {
|
||||
if ds.BuiltIn {
|
||||
continue
|
||||
}
|
||||
// filter out built in plugins
|
||||
if pluginDef.BuiltIn {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, listItem)
|
||||
@ -103,32 +105,32 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
|
||||
func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Response {
|
||||
pluginID := web.Params(c.Req)[":pluginId"]
|
||||
|
||||
def := hs.PluginManager.GetPlugin(pluginID)
|
||||
def := hs.pluginStore.Plugin(pluginID)
|
||||
if def == nil {
|
||||
return response.Error(404, "Plugin not found, no installed plugin with that id", nil)
|
||||
}
|
||||
|
||||
dto := &dtos.PluginSetting{
|
||||
Type: def.Type,
|
||||
Id: def.Id,
|
||||
Type: string(def.Type),
|
||||
Id: def.ID,
|
||||
Name: def.Name,
|
||||
Info: &def.Info,
|
||||
Dependencies: &def.Dependencies,
|
||||
Includes: def.Includes,
|
||||
BaseUrl: def.BaseUrl,
|
||||
BaseUrl: def.BaseURL,
|
||||
Module: def.Module,
|
||||
DefaultNavUrl: def.DefaultNavUrl,
|
||||
LatestVersion: def.GrafanaNetVersion,
|
||||
HasUpdate: def.GrafanaNetHasUpdate,
|
||||
DefaultNavUrl: def.DefaultNavURL,
|
||||
LatestVersion: def.GrafanaComVersion,
|
||||
HasUpdate: def.GrafanaComHasUpdate,
|
||||
State: def.State,
|
||||
Signature: def.Signature,
|
||||
SignatureType: def.SignatureType,
|
||||
SignatureOrg: def.SignatureOrg,
|
||||
}
|
||||
|
||||
if app := hs.PluginManager.GetApp(def.Id); app != nil {
|
||||
dto.Enabled = app.AutoEnabled
|
||||
dto.Pinned = app.AutoEnabled
|
||||
if def.IsApp() {
|
||||
dto.Enabled = def.AutoEnabled
|
||||
dto.Pinned = def.AutoEnabled
|
||||
}
|
||||
|
||||
query := models.GetPluginSettingByIdQuery{PluginId: pluginID, OrgId: c.OrgId}
|
||||
@ -148,7 +150,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon
|
||||
func (hs *HTTPServer) UpdatePluginSetting(c *models.ReqContext, cmd models.UpdatePluginSettingCmd) response.Response {
|
||||
pluginID := web.Params(c.Req)[":pluginId"]
|
||||
|
||||
if app := hs.PluginManager.GetApp(pluginID); app == nil {
|
||||
if app := hs.pluginStore.Plugin(pluginID); app == nil {
|
||||
return response.Error(404, "Plugin not installed", nil)
|
||||
}
|
||||
|
||||
@ -164,9 +166,9 @@ func (hs *HTTPServer) UpdatePluginSetting(c *models.ReqContext, cmd models.Updat
|
||||
func (hs *HTTPServer) GetPluginDashboards(c *models.ReqContext) response.Response {
|
||||
pluginID := web.Params(c.Req)[":pluginId"]
|
||||
|
||||
list, err := hs.PluginManager.GetPluginDashboards(c.OrgId, pluginID)
|
||||
list, err := hs.pluginDashboardManager.GetPluginDashboards(c.OrgId, pluginID)
|
||||
if err != nil {
|
||||
var notFound plugins.PluginNotFoundError
|
||||
var notFound plugins.NotFoundError
|
||||
if errors.As(err, ¬Found) {
|
||||
return response.Error(404, notFound.Error(), nil)
|
||||
}
|
||||
@ -181,9 +183,9 @@ func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response
|
||||
pluginID := web.Params(c.Req)[":pluginId"]
|
||||
name := web.Params(c.Req)[":name"]
|
||||
|
||||
content, err := hs.PluginManager.GetPluginMarkdown(pluginID, name)
|
||||
content, err := hs.pluginMarkdown(pluginID, name)
|
||||
if err != nil {
|
||||
var notFound plugins.PluginNotFoundError
|
||||
var notFound plugins.NotFoundError
|
||||
if errors.As(err, ¬Found) {
|
||||
return response.Error(404, notFound.Error(), nil)
|
||||
}
|
||||
@ -193,7 +195,7 @@ func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response
|
||||
|
||||
// fallback try readme
|
||||
if len(content) == 0 {
|
||||
content, err = hs.PluginManager.GetPluginMarkdown(pluginID, "readme")
|
||||
content, err = hs.pluginMarkdown(pluginID, "readme")
|
||||
if err != nil {
|
||||
return response.Error(501, "Could not get markdown file", err)
|
||||
}
|
||||
@ -218,8 +220,8 @@ func (hs *HTTPServer) ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDa
|
||||
}
|
||||
}
|
||||
|
||||
dashInfo, dash, err := hs.PluginManager.ImportDashboard(apiCmd.PluginId, apiCmd.Path, c.OrgId, apiCmd.FolderId,
|
||||
apiCmd.Dashboard, apiCmd.Overwrite, apiCmd.Inputs, c.SignedInUser, hs.DataService)
|
||||
dashInfo, dash, err := hs.pluginDashboardManager.ImportDashboard(apiCmd.PluginId, apiCmd.Path, c.OrgId, apiCmd.FolderId,
|
||||
apiCmd.Dashboard, apiCmd.Overwrite, apiCmd.Inputs, c.SignedInUser)
|
||||
if err != nil {
|
||||
return hs.dashboardSaveErrorToApiResponse(err)
|
||||
}
|
||||
@ -242,12 +244,12 @@ func (hs *HTTPServer) ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDa
|
||||
// /api/plugins/:pluginId/metrics
|
||||
func (hs *HTTPServer) CollectPluginMetrics(c *models.ReqContext) response.Response {
|
||||
pluginID := web.Params(c.Req)[":pluginId"]
|
||||
plugin := hs.PluginManager.GetPlugin(pluginID)
|
||||
plugin := hs.pluginStore.Plugin(pluginID)
|
||||
if plugin == nil {
|
||||
return response.Error(404, "Plugin not found", nil)
|
||||
}
|
||||
|
||||
resp, err := hs.BackendPluginManager.CollectMetrics(c.Req.Context(), plugin.Id)
|
||||
resp, err := hs.pluginClient.CollectMetrics(c.Req.Context(), plugin.ID)
|
||||
if err != nil {
|
||||
return translatePluginRequestErrorToAPIError(err)
|
||||
}
|
||||
@ -263,7 +265,7 @@ func (hs *HTTPServer) CollectPluginMetrics(c *models.ReqContext) response.Respon
|
||||
// /public/plugins/:pluginId/*
|
||||
func (hs *HTTPServer) getPluginAssets(c *models.ReqContext) {
|
||||
pluginID := web.Params(c.Req)[":pluginId"]
|
||||
plugin := hs.PluginManager.GetPlugin(pluginID)
|
||||
plugin := hs.pluginStore.Plugin(pluginID)
|
||||
if plugin == nil {
|
||||
c.JsonApiErr(404, "Plugin not found", nil)
|
||||
return
|
||||
@ -323,7 +325,9 @@ func (hs *HTTPServer) CheckHealth(c *models.ReqContext) response.Response {
|
||||
return response.Error(404, "Plugin not found", nil)
|
||||
}
|
||||
|
||||
resp, err := hs.BackendPluginManager.CheckHealth(c.Req.Context(), pCtx)
|
||||
resp, err := hs.pluginClient.CheckHealth(c.Req.Context(), &backend.CheckHealthRequest{
|
||||
PluginContext: pCtx,
|
||||
})
|
||||
if err != nil {
|
||||
return translatePluginRequestErrorToAPIError(err)
|
||||
}
|
||||
@ -366,19 +370,19 @@ func (hs *HTTPServer) CallResource(c *models.ReqContext) {
|
||||
c.JsonApiErr(404, "Plugin not found", nil)
|
||||
return
|
||||
}
|
||||
hs.BackendPluginManager.CallResource(pCtx, c, web.Params(c.Req)["*"])
|
||||
hs.pluginClient.CallResource(pCtx, c, web.Params(c.Req)["*"])
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) GetPluginErrorsList(_ *models.ReqContext) response.Response {
|
||||
return response.JSON(200, hs.PluginManager.ScanningErrors())
|
||||
return response.JSON(200, hs.pluginErrorResolver.PluginErrors())
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) InstallPlugin(c *models.ReqContext, dto dtos.InstallPluginCommand) response.Response {
|
||||
pluginID := web.Params(c.Req)[":pluginId"]
|
||||
|
||||
err := hs.PluginManager.Install(c.Req.Context(), pluginID, dto.Version)
|
||||
err := hs.pluginStore.Add(c.Req.Context(), pluginID, dto.Version, plugins.AddOpts{})
|
||||
if err != nil {
|
||||
var dupeErr plugins.DuplicatePluginError
|
||||
var dupeErr plugins.DuplicateError
|
||||
if errors.As(err, &dupeErr) {
|
||||
return response.Error(http.StatusConflict, "Plugin already installed", err)
|
||||
}
|
||||
@ -407,7 +411,7 @@ func (hs *HTTPServer) InstallPlugin(c *models.ReqContext, dto dtos.InstallPlugin
|
||||
func (hs *HTTPServer) UninstallPlugin(c *models.ReqContext) response.Response {
|
||||
pluginID := web.Params(c.Req)[":pluginId"]
|
||||
|
||||
err := hs.PluginManager.Uninstall(c.Req.Context(), pluginID)
|
||||
err := hs.pluginStore.Remove(c.Req.Context(), pluginID)
|
||||
if err != nil {
|
||||
if errors.Is(err, plugins.ErrPluginNotInstalled) {
|
||||
return response.Error(http.StatusNotFound, "Plugin not installed", err)
|
||||
@ -443,3 +447,39 @@ func translatePluginRequestErrorToAPIError(err error) response.Response {
|
||||
|
||||
return response.Error(500, "Plugin request failed", err)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) pluginMarkdown(pluginId string, name string) ([]byte, error) {
|
||||
plug := hs.pluginStore.Plugin(pluginId)
|
||||
if plug == nil {
|
||||
return nil, plugins.NotFoundError{PluginID: pluginId}
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based
|
||||
// on plugin the folder structure on disk and not user input.
|
||||
path := filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
|
||||
exists, err := fs.Exists(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
path = filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name)))
|
||||
}
|
||||
|
||||
exists, err = fs.Exists(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return make([]byte, 0), nil
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
// We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based
|
||||
// on plugin the folder structure on disk and not user input.
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@ -35,15 +34,17 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
requestedFile := filepath.Clean(tmpFile.Name())
|
||||
|
||||
t.Run("Given a request for an existing plugin file that is listed as a signature covered file", func(t *testing.T) {
|
||||
p := &plugins.PluginBase{
|
||||
Id: pluginID,
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: pluginID,
|
||||
},
|
||||
PluginDir: pluginDir,
|
||||
SignedFiles: map[string]struct{}{
|
||||
requestedFile: {},
|
||||
},
|
||||
}
|
||||
service := &pluginManager{
|
||||
plugins: map[string]*plugins.PluginBase{
|
||||
service := &pluginStore{
|
||||
plugins: map[string]*plugins.Plugin{
|
||||
pluginID: p,
|
||||
},
|
||||
}
|
||||
@ -61,12 +62,14 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Given a request for an existing plugin file that is not listed as a signature covered file", func(t *testing.T) {
|
||||
p := &plugins.PluginBase{
|
||||
Id: pluginID,
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: pluginID,
|
||||
},
|
||||
PluginDir: pluginDir,
|
||||
}
|
||||
service := &pluginManager{
|
||||
plugins: map[string]*plugins.PluginBase{
|
||||
service := &pluginStore{
|
||||
plugins: map[string]*plugins.Plugin{
|
||||
pluginID: p,
|
||||
},
|
||||
}
|
||||
@ -84,12 +87,14 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Given a request for an non-existing plugin file", func(t *testing.T) {
|
||||
p := &plugins.PluginBase{
|
||||
Id: pluginID,
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: pluginID,
|
||||
},
|
||||
PluginDir: pluginDir,
|
||||
}
|
||||
service := &pluginManager{
|
||||
plugins: map[string]*plugins.PluginBase{
|
||||
service := &pluginStore{
|
||||
plugins: map[string]*plugins.Plugin{
|
||||
pluginID: p,
|
||||
},
|
||||
}
|
||||
@ -111,8 +116,8 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Given a request for an non-existing plugin", func(t *testing.T) {
|
||||
service := &pluginManager{
|
||||
plugins: map[string]*plugins.PluginBase{},
|
||||
service := &pluginStore{
|
||||
plugins: map[string]*plugins.Plugin{},
|
||||
}
|
||||
l := &logger{}
|
||||
|
||||
@ -132,10 +137,10 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Given a request for a core plugin's file", func(t *testing.T) {
|
||||
service := &pluginManager{
|
||||
plugins: map[string]*plugins.PluginBase{
|
||||
service := &pluginStore{
|
||||
plugins: map[string]*plugins.Plugin{
|
||||
pluginID: {
|
||||
IsCorePlugin: true,
|
||||
Class: plugins.Core,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -157,15 +162,15 @@ func callGetPluginAsset(sc *scenarioContext) {
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, pluginManager plugins.Manager,
|
||||
func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, pluginStore plugins.Store,
|
||||
logger log.Logger, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
hs := HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
PluginManager: pluginManager,
|
||||
log: logger,
|
||||
Cfg: setting.NewCfg(),
|
||||
pluginStore: pluginStore,
|
||||
log: logger,
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(t, url)
|
||||
@ -180,13 +185,13 @@ func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern strin
|
||||
})
|
||||
}
|
||||
|
||||
type pluginManager struct {
|
||||
manager.PluginManager
|
||||
type pluginStore struct {
|
||||
plugins.Store
|
||||
|
||||
plugins map[string]*plugins.PluginBase
|
||||
plugins map[string]*plugins.Plugin
|
||||
}
|
||||
|
||||
func (pm *pluginManager) GetPlugin(id string) *plugins.PluginBase {
|
||||
func (pm *pluginStore) Plugin(id string) *plugins.Plugin {
|
||||
return pm.plugins[id]
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ type UsageStats struct {
|
||||
Cfg *setting.Cfg
|
||||
Bus bus.Bus
|
||||
SQLStore *sqlstore.SQLStore
|
||||
PluginManager plugins.Manager
|
||||
pluginStore plugins.Store
|
||||
SocialService social.Service
|
||||
kvStore *kvstore.NamespacedKVStore
|
||||
|
||||
@ -32,14 +32,14 @@ type UsageStats struct {
|
||||
sendReportCallbacks []usagestats.SendReportCallbackFunc
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlStore *sqlstore.SQLStore, pluginManager plugins.Manager,
|
||||
func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlStore *sqlstore.SQLStore, pluginStore plugins.Store,
|
||||
socialService social.Service, kvStore kvstore.KVStore) *UsageStats {
|
||||
s := &UsageStats{
|
||||
Cfg: cfg,
|
||||
Bus: bus,
|
||||
SQLStore: sqlStore,
|
||||
oauthProviders: socialService.GetOAuthProviders(),
|
||||
PluginManager: pluginManager,
|
||||
pluginStore: pluginStore,
|
||||
kvStore: kvstore.WithNamespace(kvStore, 0, "infra.usagestats"),
|
||||
log: log.New("infra.usagestats"),
|
||||
startTime: time.Now(),
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
|
||||
@ -50,9 +51,9 @@ func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, e
|
||||
metrics["stats.viewers.count"] = statsQuery.Result.Viewers
|
||||
metrics["stats.orgs.count"] = statsQuery.Result.Orgs
|
||||
metrics["stats.playlist.count"] = statsQuery.Result.Playlists
|
||||
metrics["stats.plugins.apps.count"] = uss.PluginManager.AppCount()
|
||||
metrics["stats.plugins.panels.count"] = uss.PluginManager.PanelCount()
|
||||
metrics["stats.plugins.datasources.count"] = uss.PluginManager.DataSourceCount()
|
||||
metrics["stats.plugins.apps.count"] = uss.appCount()
|
||||
metrics["stats.plugins.panels.count"] = uss.panelCount()
|
||||
metrics["stats.plugins.datasources.count"] = uss.dataSourceCount()
|
||||
metrics["stats.alerts.count"] = statsQuery.Result.Alerts
|
||||
metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
|
||||
metrics["stats.active_admins.count"] = statsQuery.Result.ActiveAdmins
|
||||
@ -328,7 +329,7 @@ func (uss *UsageStats) updateTotalStats(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (uss *UsageStats) ShouldBeReported(dsType string) bool {
|
||||
ds := uss.PluginManager.GetDataSource(dsType)
|
||||
ds := uss.pluginStore.Plugin(dsType)
|
||||
if ds == nil {
|
||||
return false
|
||||
}
|
||||
@ -363,3 +364,15 @@ func (uss *UsageStats) GetUsageStatsId(ctx context.Context) string {
|
||||
|
||||
return anonId
|
||||
}
|
||||
|
||||
func (uss *UsageStats) appCount() int {
|
||||
return len(uss.pluginStore.Plugins(plugins.App))
|
||||
}
|
||||
|
||||
func (uss *UsageStats) panelCount() int {
|
||||
return len(uss.pluginStore.Plugins(plugins.Panel))
|
||||
}
|
||||
|
||||
func (uss *UsageStats) dataSourceCount() int {
|
||||
return len(uss.pluginStore.Plugins(plugins.DataSource))
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -318,9 +317,9 @@ func TestMetrics(t *testing.T) {
|
||||
assert.Equal(t, getSystemStatsQuery.Result.Viewers, metrics.Get("stats.viewers.count").MustInt64())
|
||||
assert.Equal(t, getSystemStatsQuery.Result.Orgs, metrics.Get("stats.orgs.count").MustInt64())
|
||||
assert.Equal(t, getSystemStatsQuery.Result.Playlists, metrics.Get("stats.playlist.count").MustInt64())
|
||||
assert.Equal(t, uss.PluginManager.AppCount(), metrics.Get("stats.plugins.apps.count").MustInt())
|
||||
assert.Equal(t, uss.PluginManager.PanelCount(), metrics.Get("stats.plugins.panels.count").MustInt())
|
||||
assert.Equal(t, uss.PluginManager.DataSourceCount(), metrics.Get("stats.plugins.datasources.count").MustInt())
|
||||
assert.Equal(t, uss.appCount(), metrics.Get("stats.plugins.apps.count").MustInt())
|
||||
assert.Equal(t, uss.panelCount(), metrics.Get("stats.plugins.panels.count").MustInt())
|
||||
assert.Equal(t, uss.dataSourceCount(), metrics.Get("stats.plugins.datasources.count").MustInt())
|
||||
assert.Equal(t, getSystemStatsQuery.Result.Alerts, metrics.Get("stats.alerts.count").MustInt64())
|
||||
assert.Equal(t, getSystemStatsQuery.Result.ActiveUsers, metrics.Get("stats.active_users.count").MustInt64())
|
||||
assert.Equal(t, getSystemStatsQuery.Result.ActiveAdmins, metrics.Get("stats.active_admins.count").MustInt64())
|
||||
@ -552,57 +551,45 @@ func TestMetrics(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
type fakePluginManager struct {
|
||||
manager.PluginManager
|
||||
type fakePluginStore struct {
|
||||
plugins.Store
|
||||
|
||||
dataSources map[string]*plugins.DataSourcePlugin
|
||||
panels map[string]*plugins.PanelPlugin
|
||||
plugins map[string]*plugins.Plugin
|
||||
}
|
||||
|
||||
func (pm *fakePluginManager) DataSourceCount() int {
|
||||
return len(pm.dataSources)
|
||||
func (pr fakePluginStore) Plugin(pluginID string) *plugins.Plugin {
|
||||
return pr.plugins[pluginID]
|
||||
}
|
||||
|
||||
func (pm *fakePluginManager) GetDataSource(id string) *plugins.DataSourcePlugin {
|
||||
return pm.dataSources[id]
|
||||
}
|
||||
func (pr fakePluginStore) Plugins(pluginTypes ...plugins.Type) []*plugins.Plugin {
|
||||
var result []*plugins.Plugin
|
||||
for _, v := range pr.plugins {
|
||||
for _, t := range pluginTypes {
|
||||
if v.Type == t {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pm *fakePluginManager) PanelCount() int {
|
||||
return len(pm.panels)
|
||||
return result
|
||||
}
|
||||
|
||||
func setupSomeDataSourcePlugins(t *testing.T, uss *UsageStats) {
|
||||
t.Helper()
|
||||
|
||||
uss.PluginManager = &fakePluginManager{
|
||||
dataSources: map[string]*plugins.DataSourcePlugin{
|
||||
uss.pluginStore = &fakePluginStore{
|
||||
plugins: map[string]*plugins.Plugin{
|
||||
models.DS_ES: {
|
||||
FrontendPluginBase: plugins.FrontendPluginBase{
|
||||
PluginBase: plugins.PluginBase{
|
||||
Signature: "internal",
|
||||
},
|
||||
},
|
||||
Signature: "internal",
|
||||
},
|
||||
models.DS_PROMETHEUS: {
|
||||
FrontendPluginBase: plugins.FrontendPluginBase{
|
||||
PluginBase: plugins.PluginBase{
|
||||
Signature: "internal",
|
||||
},
|
||||
},
|
||||
Signature: "internal",
|
||||
},
|
||||
models.DS_GRAPHITE: {
|
||||
FrontendPluginBase: plugins.FrontendPluginBase{
|
||||
PluginBase: plugins.PluginBase{
|
||||
Signature: "internal",
|
||||
},
|
||||
},
|
||||
Signature: "internal",
|
||||
},
|
||||
models.DS_MYSQL: {
|
||||
FrontendPluginBase: plugins.FrontendPluginBase{
|
||||
PluginBase: plugins.PluginBase{
|
||||
Signature: "internal",
|
||||
},
|
||||
},
|
||||
Signature: "internal",
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -624,7 +611,7 @@ func createService(t *testing.T, cfg setting.Cfg) *UsageStats {
|
||||
Cfg: &cfg,
|
||||
SQLStore: sqlStore,
|
||||
externalMetrics: make([]usagestats.MetricsFunc, 0),
|
||||
PluginManager: &fakePluginManager{},
|
||||
pluginStore: &fakePluginStore{},
|
||||
kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"),
|
||||
log: log.New("infra.usagestats"),
|
||||
startTime: time.Now().Add(-1 * time.Minute),
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
@ -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"
|
||||
)
|
15
pkg/plugins/manager/installer/ifaces.go
Normal file
15
pkg/plugins/manager/installer/ifaces.go
Normal file
@ -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
|
||||
|
91
pkg/plugins/manager/loader/finder/finder.go
Normal file
91
pkg/plugins/manager/loader/finder/finder.go
Normal file
@ -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
|
||||
}
|
67
pkg/plugins/manager/loader/finder/finder_test.go
Normal file
67
pkg/plugins/manager/loader/finder/finder_test.go
Normal file
@ -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]))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
273
pkg/plugins/manager/loader/initializer/initializer.go
Normal file
273
pkg/plugins/manager/loader/initializer/initializer.go
Normal file
@ -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
|
||||
}
|
349
pkg/plugins/manager/loader/initializer/initializer_test.go
Normal file
349
pkg/plugins/manager/loader/initializer/initializer_test.go
Normal file
@ -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
|
||||
}
|
297
pkg/plugins/manager/loader/loader.go
Normal file
297
pkg/plugins/manager/loader/loader.go
Normal file
@ -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{}{}
|
||||
}
|
||||
}
|
928
pkg/plugins/manager/loader/loader_test.go
Normal file
928
pkg/plugins/manager/loader/loader_test.go
Normal file
@ -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),
|
||||
|
File diff suppressed because it is too large
Load Diff
165
pkg/plugins/manager/manager_integration_test.go
Normal file
165
pkg/plugins/manager/manager_integration_test.go
Normal file
@ -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)
|
||||
}
|
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
|
||||
}
|
34
pkg/plugins/manager/signature/authorizer.go
Normal file
34
pkg/plugins/manager/signature/authorizer.go
Normal file
@ -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 {
|
@ -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)
|
81
pkg/plugins/manager/signature/signature.go
Normal file
81
pkg/plugins/manager/signature/signature.go
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
4
pkg/plugins/manager/testdata/invalid-plugin-json/plugin.json
vendored
Normal file
4
pkg/plugins/manager/testdata/invalid-plugin-json/plugin.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"id": "test-app",
|
||||
"type": "application"
|
||||
}
|
28
pkg/plugins/manager/testdata/nested-plugins/parent/MANIFEST.txt
vendored
Normal file
28
pkg/plugins/manager/testdata/nested-plugins/parent/MANIFEST.txt
vendored
Normal file
@ -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-----
|
14
pkg/plugins/manager/testdata/nested-plugins/parent/nested/plugin.json
vendored
Normal file
14
pkg/plugins/manager/testdata/nested-plugins/parent/nested/plugin.json
vendored
Normal file
@ -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"
|
||||
}
|
||||
}
|
15
pkg/plugins/manager/testdata/nested-plugins/parent/plugin.json
vendored
Normal file
15
pkg/plugins/manager/testdata/nested-plugins/parent/plugin.json
vendored
Normal file
@ -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)
|
||||
}
|
||||
}()
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *PluginManager) externalPluginIDsAsCSV() string {
|
||||
var result []string
|
||||
for _, p := range m.plugins {
|
||||
if p.IsCorePlugin() {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, p.ID)
|
||||
}
|
||||
|
||||
return strings.Join(result, ",")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -7,7 +7,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
uss "github.com/grafana/grafana/pkg/infra/usagestats/service"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
backendmanager "github.com/grafana/grafana/pkg/plugins/backendplugin/manager"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindashboards"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
@ -22,6 +21,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/services/updatechecker"
|
||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudmonitoring"
|
||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch"
|
||||
@ -44,12 +44,12 @@ func ProvideBackgroundServiceRegistry(
|
||||
live *live.GrafanaLive, pushGateway *pushhttp.Gateway, notifications *notifications.NotificationService,
|
||||
rendering *rendering.RenderingService, tokenService models.UserTokenBackgroundService,
|
||||
provisioning *provisioning.ProvisioningServiceImpl, alerting *alerting.AlertEngine, pm *manager.PluginManager,
|
||||
backendPM *backendmanager.Manager, metrics *metrics.InternalMetricsService,
|
||||
usageStats *uss.UsageStats, tracing *tracing.TracingService, remoteCache *remotecache.RemoteCache,
|
||||
metrics *metrics.InternalMetricsService, usageStats *uss.UsageStats, updateChecker *updatechecker.Service,
|
||||
tracing *tracing.TracingService, remoteCache *remotecache.RemoteCache,
|
||||
// Need to make sure these are initialized, is there a better place to put them?
|
||||
_ *azuremonitor.Service, _ *cloudwatch.CloudWatchService, _ *elasticsearch.Service, _ *graphite.Service,
|
||||
_ *influxdb.Service, _ *loki.Service, _ *opentsdb.Service, _ *prometheus.Service, _ *tempo.Service,
|
||||
_ *testdatasource.TestDataPlugin, _ *plugindashboards.Service, _ *dashboardsnapshots.Service, _ secrets.Service,
|
||||
_ *testdatasource.Service, _ *plugindashboards.Service, _ *dashboardsnapshots.Service, _ secrets.Service,
|
||||
_ *postgres.Service, _ *mysql.Service, _ *mssql.Service, _ *grafanads.Service, _ *cloudmonitoring.Service,
|
||||
_ *pluginsettings.Service, _ *alerting.AlertNotificationService,
|
||||
) *BackgroundServiceRegistry {
|
||||
@ -65,7 +65,7 @@ func ProvideBackgroundServiceRegistry(
|
||||
provisioning,
|
||||
alerting,
|
||||
pm,
|
||||
backendPM,
|
||||
updateChecker,
|
||||
metrics,
|
||||
usageStats,
|
||||
tracing,
|
||||
|
@ -22,9 +22,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
backendmanager "github.com/grafana/grafana/pkg/plugins/backendplugin/manager"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugincontext"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindashboards"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
@ -56,6 +55,7 @@ import (
|
||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||
"github.com/grafana/grafana/pkg/services/shorturls"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/updatechecker"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor"
|
||||
@ -92,12 +92,19 @@ var wireBasicSet = wire.NewSet(
|
||||
hooks.ProvideService,
|
||||
kvstore.ProvideService,
|
||||
localcache.ProvideService,
|
||||
updatechecker.ProvideService,
|
||||
uss.ProvideService,
|
||||
wire.Bind(new(usagestats.Service), new(*uss.UsageStats)),
|
||||
manager.ProvideService,
|
||||
wire.Bind(new(plugins.Manager), new(*manager.PluginManager)),
|
||||
backendmanager.ProvideService,
|
||||
wire.Bind(new(backendplugin.Manager), new(*backendmanager.Manager)),
|
||||
wire.Bind(new(plugins.Client), new(*manager.PluginManager)),
|
||||
wire.Bind(new(plugins.Store), new(*manager.PluginManager)),
|
||||
wire.Bind(new(plugins.CoreBackendRegistrar), new(*manager.PluginManager)),
|
||||
wire.Bind(new(plugins.StaticRouteResolver), new(*manager.PluginManager)),
|
||||
wire.Bind(new(plugins.PluginDashboardManager), new(*manager.PluginManager)),
|
||||
wire.Bind(new(plugins.RendererManager), new(*manager.PluginManager)),
|
||||
loader.ProvideService,
|
||||
wire.Bind(new(plugins.Loader), new(*loader.Loader)),
|
||||
wire.Bind(new(plugins.ErrorResolver), new(*loader.Loader)),
|
||||
cloudwatch.ProvideService,
|
||||
cloudwatch.ProvideLogsService,
|
||||
cloudmonitoring.ProvideService,
|
||||
|
@ -6,6 +6,8 @@ package server
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/server/backgroundsvcs"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
@ -54,6 +56,8 @@ var wireExtsBasicSet = wire.NewSet(
|
||||
wire.Bind(new(models.SearchUserFilter), new(*filters.OSSSearchUserFilter)),
|
||||
searchusers.ProvideUsersService,
|
||||
wire.Bind(new(searchusers.Service), new(*searchusers.OSSService)),
|
||||
signature.ProvideService,
|
||||
wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)),
|
||||
)
|
||||
|
||||
var wireExtsSet = wire.NewSet(
|
||||
|
@ -18,12 +18,12 @@ import (
|
||||
)
|
||||
|
||||
func ProvideService(dataSourceCache datasources.CacheService, plugReqValidator models.PluginRequestValidator,
|
||||
pm plugins.Manager, cfg *setting.Cfg, httpClientProvider httpclient.Provider,
|
||||
pluginStore plugins.Store, cfg *setting.Cfg, httpClientProvider httpclient.Provider,
|
||||
oauthTokenService *oauthtoken.Service, dsService *datasources.Service) *DataSourceProxyService {
|
||||
return &DataSourceProxyService{
|
||||
DataSourceCache: dataSourceCache,
|
||||
PluginRequestValidator: plugReqValidator,
|
||||
PluginManager: pm,
|
||||
pluginStore: pluginStore,
|
||||
Cfg: cfg,
|
||||
HTTPClientProvider: httpClientProvider,
|
||||
OAuthTokenService: oauthTokenService,
|
||||
@ -34,7 +34,7 @@ func ProvideService(dataSourceCache datasources.CacheService, plugReqValidator m
|
||||
type DataSourceProxyService struct {
|
||||
DataSourceCache datasources.CacheService
|
||||
PluginRequestValidator models.PluginRequestValidator
|
||||
PluginManager plugins.Manager
|
||||
pluginStore plugins.Store
|
||||
Cfg *setting.Cfg
|
||||
HTTPClientProvider httpclient.Provider
|
||||
OAuthTokenService *oauthtoken.Service
|
||||
@ -69,16 +69,15 @@ func (p *DataSourceProxyService) ProxyDatasourceRequestWithID(c *models.ReqConte
|
||||
}
|
||||
|
||||
// find plugin
|
||||
plugin := p.PluginManager.GetDataSource(ds.Type)
|
||||
plugin := p.pluginStore.Plugin(ds.Type)
|
||||
if plugin == nil {
|
||||
c.JsonApiErr(http.StatusNotFound, "Unable to find datasource plugin", err)
|
||||
return
|
||||
}
|
||||
|
||||
proxy, err := pluginproxy.NewDataSourceProxy(
|
||||
ds, plugin, c, getProxyPath(c), p.Cfg, p.HTTPClientProvider, p.OAuthTokenService, p.DataSourcesService,
|
||||
)
|
||||
|
||||
proxyPath := getProxyPath(c)
|
||||
proxy, err := pluginproxy.NewDataSourceProxy(ds, plugin.Routes, c, proxyPath, p.Cfg, p.HTTPClientProvider,
|
||||
p.OAuthTokenService, p.DataSourcesService)
|
||||
if err != nil {
|
||||
if errors.Is(err, datasource.URLValidationError{}) {
|
||||
c.JsonApiErr(http.StatusBadRequest, fmt.Sprintf("Invalid data source URL: %q", ds.Url), err)
|
||||
|
@ -26,7 +26,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugincontext"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/live/database"
|
||||
@ -61,7 +61,7 @@ type CoreGrafanaScope struct {
|
||||
}
|
||||
|
||||
func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, routeRegister routing.RouteRegister,
|
||||
logsService *cloudwatch.LogsService, pluginManager *manager.PluginManager, cacheService *localcache.CacheService,
|
||||
logsService *cloudwatch.LogsService, pluginStore plugins.Store, cacheService *localcache.CacheService,
|
||||
dataSourceCache datasources.CacheService, sqlStore *sqlstore.SQLStore,
|
||||
usageStatsService usagestats.Service) (*GrafanaLive, error) {
|
||||
g := &GrafanaLive{
|
||||
@ -69,7 +69,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
|
||||
PluginContextProvider: plugCtxProvider,
|
||||
RouteRegister: routeRegister,
|
||||
LogsService: logsService,
|
||||
PluginManager: pluginManager,
|
||||
pluginStore: pluginStore,
|
||||
CacheService: cacheService,
|
||||
DataSourceCache: dataSourceCache,
|
||||
SQLStore: sqlStore,
|
||||
@ -361,10 +361,10 @@ type GrafanaLive struct {
|
||||
Cfg *setting.Cfg
|
||||
RouteRegister routing.RouteRegister
|
||||
LogsService *cloudwatch.LogsService
|
||||
PluginManager *manager.PluginManager
|
||||
CacheService *localcache.CacheService
|
||||
DataSourceCache datasources.CacheService
|
||||
SQLStore *sqlstore.SQLStore
|
||||
pluginStore plugins.Store
|
||||
|
||||
node *centrifuge.Node
|
||||
surveyCaller *survey.Caller
|
||||
@ -393,15 +393,14 @@ type GrafanaLive struct {
|
||||
}
|
||||
|
||||
func (g *GrafanaLive) getStreamPlugin(pluginID string) (backend.StreamHandler, error) {
|
||||
plugin, ok := g.PluginManager.BackendPluginManager.Get(pluginID)
|
||||
if !ok {
|
||||
plugin := g.pluginStore.Plugin(pluginID)
|
||||
if plugin == nil {
|
||||
return nil, fmt.Errorf("plugin not found: %s", pluginID)
|
||||
}
|
||||
streamHandler, ok := plugin.(backend.StreamHandler)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s plugin does not implement StreamHandler: %#v", pluginID, plugin)
|
||||
if plugin.SupportsStreaming() {
|
||||
return plugin, nil
|
||||
}
|
||||
return streamHandler, nil
|
||||
return nil, fmt.Errorf("%s plugin does not implement StreamHandler: %#v", pluginID, plugin)
|
||||
}
|
||||
|
||||
func (g *GrafanaLive) Run(ctx context.Context) error {
|
||||
|
@ -17,12 +17,12 @@ type configReader interface {
|
||||
}
|
||||
|
||||
type configReaderImpl struct {
|
||||
log log.Logger
|
||||
pluginManager plugins.Manager
|
||||
log log.Logger
|
||||
pluginStore plugins.Store
|
||||
}
|
||||
|
||||
func newConfigReader(logger log.Logger, pluginManager plugins.Manager) configReader {
|
||||
return &configReaderImpl{log: logger, pluginManager: pluginManager}
|
||||
func newConfigReader(logger log.Logger, pluginStore plugins.Store) configReader {
|
||||
return &configReaderImpl{log: logger, pluginStore: pluginStore}
|
||||
}
|
||||
|
||||
func (cr *configReaderImpl) readConfig(path string) ([]*pluginsAsConfig, error) {
|
||||
@ -113,8 +113,8 @@ func (cr *configReaderImpl) validatePluginsConfig(apps []*pluginsAsConfig) error
|
||||
}
|
||||
|
||||
for _, app := range apps[i].Apps {
|
||||
if !cr.pluginManager.IsAppInstalled(app.PluginID) {
|
||||
return fmt.Errorf("app plugin not installed: %q", app.PluginID)
|
||||
if p := cr.pluginStore.Plugin(app.PluginID); p == nil {
|
||||
return fmt.Errorf("plugin not installed: %q", app.PluginID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,10 +32,10 @@ func TestConfigReader(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Unknown app plugin should return error", func(t *testing.T) {
|
||||
cfgProvider := newConfigReader(log.New("test logger"), fakePluginManager{})
|
||||
cfgProvider := newConfigReader(log.New("test logger"), fakePluginStore{})
|
||||
_, err := cfgProvider.readConfig(unknownApp)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "app plugin not installed: \"nonexisting\"", err.Error())
|
||||
require.Equal(t, "plugin not installed: \"nonexisting\"", err.Error())
|
||||
})
|
||||
|
||||
t.Run("Read incorrect properties", func(t *testing.T) {
|
||||
@ -46,8 +46,8 @@ func TestConfigReader(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Can read correct properties", func(t *testing.T) {
|
||||
pm := fakePluginManager{
|
||||
apps: map[string]*plugins.AppPlugin{
|
||||
pm := fakePluginStore{
|
||||
apps: map[string]*plugins.Plugin{
|
||||
"test-plugin": {},
|
||||
"test-plugin-2": {},
|
||||
},
|
||||
@ -87,13 +87,12 @@ func TestConfigReader(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
type fakePluginManager struct {
|
||||
plugins.Manager
|
||||
type fakePluginStore struct {
|
||||
plugins.Store
|
||||
|
||||
apps map[string]*plugins.AppPlugin
|
||||
apps map[string]*plugins.Plugin
|
||||
}
|
||||
|
||||
func (pm fakePluginManager) IsAppInstalled(id string) bool {
|
||||
_, exists := pm.apps[id]
|
||||
return exists
|
||||
func (pr fakePluginStore) Plugin(pluginID string) *plugins.Plugin {
|
||||
return pr.apps[pluginID]
|
||||
}
|
||||
|
@ -11,11 +11,11 @@ import (
|
||||
|
||||
// Provision scans a directory for provisioning config files
|
||||
// and provisions the app in those files.
|
||||
func Provision(configDirectory string, pluginManager plugins.Manager) error {
|
||||
func Provision(configDirectory string, pluginStore plugins.Store) error {
|
||||
logger := log.New("provisioning.plugins")
|
||||
ap := PluginProvisioner{
|
||||
log: logger,
|
||||
cfgProvider: newConfigReader(logger, pluginManager),
|
||||
cfgProvider: newConfigReader(logger, pluginStore),
|
||||
}
|
||||
return ap.applyChanges(configDirectory)
|
||||
}
|
||||
|
@ -18,12 +18,12 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginManager plugifaces.Manager,
|
||||
func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore plugifaces.Store,
|
||||
encryptionService encryption.Service) (*ProvisioningServiceImpl, error) {
|
||||
s := &ProvisioningServiceImpl{
|
||||
Cfg: cfg,
|
||||
SQLStore: sqlStore,
|
||||
PluginManager: pluginManager,
|
||||
pluginStore: pluginStore,
|
||||
EncryptionService: encryptionService,
|
||||
log: log.New("provisioning"),
|
||||
newDashboardProvisioner: dashboards.New,
|
||||
@ -61,7 +61,7 @@ func newProvisioningServiceImpl(
|
||||
newDashboardProvisioner dashboards.DashboardProvisionerFactory,
|
||||
provisionNotifiers func(string, encryption.Service) error,
|
||||
provisionDatasources func(context.Context, string) error,
|
||||
provisionPlugins func(string, plugifaces.Manager) error,
|
||||
provisionPlugins func(string, plugifaces.Store) error,
|
||||
) *ProvisioningServiceImpl {
|
||||
return &ProvisioningServiceImpl{
|
||||
log: log.New("provisioning"),
|
||||
@ -75,7 +75,7 @@ func newProvisioningServiceImpl(
|
||||
type ProvisioningServiceImpl struct {
|
||||
Cfg *setting.Cfg
|
||||
SQLStore *sqlstore.SQLStore
|
||||
PluginManager plugifaces.Manager
|
||||
pluginStore plugifaces.Store
|
||||
EncryptionService encryption.Service
|
||||
log log.Logger
|
||||
pollingCtxCancel context.CancelFunc
|
||||
@ -83,7 +83,7 @@ type ProvisioningServiceImpl struct {
|
||||
dashboardProvisioner dashboards.DashboardProvisioner
|
||||
provisionNotifiers func(string, encryption.Service) error
|
||||
provisionDatasources func(context.Context, string) error
|
||||
provisionPlugins func(string, plugifaces.Manager) error
|
||||
provisionPlugins func(string, plugifaces.Store) error
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ func (ps *ProvisioningServiceImpl) ProvisionDatasources(ctx context.Context) err
|
||||
|
||||
func (ps *ProvisioningServiceImpl) ProvisionPlugins() error {
|
||||
appPath := filepath.Join(ps.Cfg.ProvisioningPath, "plugins")
|
||||
err := ps.provisionPlugins(appPath, ps.PluginManager)
|
||||
err := ps.provisionPlugins(appPath, ps.pluginStore)
|
||||
return errutil.Wrap("app provisioning error", err)
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,7 @@ func (rs *RenderingService) renderViaPlugin(ctx context.Context, renderKey strin
|
||||
}
|
||||
rs.log.Debug("Calling renderer plugin", "req", req)
|
||||
|
||||
rsp, err := rs.pluginInfo.GrpcPluginV2.Render(ctx, req)
|
||||
rsp, err := rs.pluginInfo.Renderer.Render(ctx, req)
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
rs.log.Info("Rendering timed out")
|
||||
return nil, ErrTimeout
|
||||
@ -88,7 +88,7 @@ func (rs *RenderingService) renderCSVViaPlugin(ctx context.Context, renderKey st
|
||||
}
|
||||
rs.log.Debug("Calling renderer plugin", "req", req)
|
||||
|
||||
rsp, err := rs.pluginInfo.GrpcPluginV2.RenderCSV(ctx, req)
|
||||
rsp, err := rs.pluginInfo.Renderer.RenderCSV(ctx, req)
|
||||
if err != nil {
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
rs.log.Info("Rendering timed out")
|
||||
|
@ -13,10 +13,9 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -38,19 +37,19 @@ type RenderUser struct {
|
||||
|
||||
type RenderingService struct {
|
||||
log log.Logger
|
||||
pluginInfo *plugins.RendererPlugin
|
||||
pluginInfo *plugins.Plugin
|
||||
renderAction renderFunc
|
||||
renderCSVAction renderCSVFunc
|
||||
domain string
|
||||
inProgressCount int32
|
||||
version string
|
||||
|
||||
Cfg *setting.Cfg
|
||||
RemoteCacheService *remotecache.RemoteCache
|
||||
PluginManager plugins.Manager
|
||||
Cfg *setting.Cfg
|
||||
RemoteCacheService *remotecache.RemoteCache
|
||||
RendererPluginManager plugins.RendererManager
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, pm plugins.Manager) (*RenderingService, error) {
|
||||
func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, rm plugins.RendererManager) (*RenderingService, error) {
|
||||
// ensure ImagesDir exists
|
||||
err := os.MkdirAll(cfg.ImagesDir, 0700)
|
||||
if err != nil {
|
||||
@ -81,11 +80,11 @@ func ProvideService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache, pm p
|
||||
}
|
||||
|
||||
s := &RenderingService{
|
||||
Cfg: cfg,
|
||||
RemoteCacheService: remoteCache,
|
||||
PluginManager: pm,
|
||||
log: log.New("rendering"),
|
||||
domain: domain,
|
||||
Cfg: cfg,
|
||||
RemoteCacheService: remoteCache,
|
||||
RendererPluginManager: rm,
|
||||
log: log.New("rendering"),
|
||||
domain: domain,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
@ -109,7 +108,7 @@ func (rs *RenderingService) Run(ctx context.Context) error {
|
||||
|
||||
if rs.pluginAvailable() {
|
||||
rs.log = rs.log.New("renderer", "plugin")
|
||||
rs.pluginInfo = rs.PluginManager.Renderer()
|
||||
rs.pluginInfo = rs.RendererPluginManager.Renderer()
|
||||
|
||||
if err := rs.startPlugin(ctx); err != nil {
|
||||
return err
|
||||
@ -142,7 +141,7 @@ func (rs *RenderingService) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (rs *RenderingService) pluginAvailable() bool {
|
||||
return rs.PluginManager.Renderer() != nil
|
||||
return rs.RendererPluginManager.Renderer() != nil
|
||||
}
|
||||
|
||||
func (rs *RenderingService) remoteAvailable() bool {
|
||||
@ -157,7 +156,7 @@ func (rs *RenderingService) Version() string {
|
||||
return rs.version
|
||||
}
|
||||
|
||||
func (rs *RenderingService) RenderErrorImage(err error) (*RenderResult, error) {
|
||||
func (rs *RenderingService) RenderErrorImage(_ error) (*RenderResult, error) {
|
||||
imgUrl := "public/img/rendering_error.png"
|
||||
imgPath := filepath.Join(setting.HomePath, imgUrl)
|
||||
if _, err := os.Stat(imgPath); errors.Is(err, os.ErrNotExist) {
|
||||
|
120
pkg/services/updatechecker/grafana_update_checker.go
Normal file
120
pkg/services/updatechecker/grafana_update_checker.go
Normal file
@ -0,0 +1,120 @@
|
||||
package updatechecker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
var (
|
||||
httpClient = http.Client{Timeout: 10 * time.Second}
|
||||
logger = log.New("update.checker")
|
||||
)
|
||||
|
||||
type latestJSON struct {
|
||||
Stable string `json:"stable"`
|
||||
Testing string `json:"testing"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
cfg *setting.Cfg
|
||||
|
||||
hasUpdate bool
|
||||
latestVersion string
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg) *Service {
|
||||
s := newUpdateChecker(cfg)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func newUpdateChecker(cfg *setting.Cfg) *Service {
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) IsDisabled() bool {
|
||||
return !s.cfg.CheckForUpdates
|
||||
}
|
||||
|
||||
func (s *Service) Run(ctx context.Context) error {
|
||||
s.checkForUpdates()
|
||||
|
||||
ticker := time.NewTicker(time.Minute * 10)
|
||||
run := true
|
||||
|
||||
for run {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.checkForUpdates()
|
||||
case <-ctx.Done():
|
||||
run = false
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func (s *Service) checkForUpdates() {
|
||||
resp, err := httpClient.Get("https://raw.githubusercontent.com/grafana/grafana/main/latest.json")
|
||||
if err != nil {
|
||||
logger.Debug("Failed to get latest.json repo from github.com", "error", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logger.Warn("Failed to close response body", "err", err)
|
||||
}
|
||||
}()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logger.Debug("Update check failed, reading response from github.com", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
var latest latestJSON
|
||||
err = json.Unmarshal(body, &latest)
|
||||
if err != nil {
|
||||
logger.Debug("Failed to unmarshal latest.json", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
if strings.Contains(s.cfg.BuildVersion, "-") {
|
||||
s.latestVersion = latest.Testing
|
||||
s.hasUpdate = !strings.HasPrefix(s.cfg.BuildVersion, latest.Testing)
|
||||
} else {
|
||||
s.latestVersion = latest.Stable
|
||||
s.hasUpdate = latest.Stable != s.cfg.BuildVersion
|
||||
}
|
||||
|
||||
currVersion, err1 := version.NewVersion(s.cfg.BuildVersion)
|
||||
latestVersion, err2 := version.NewVersion(s.latestVersion)
|
||||
if err1 == nil && err2 == nil {
|
||||
s.hasUpdate = currVersion.LessThan(latestVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GrafanaUpdateAvailable() bool {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
return s.hasUpdate
|
||||
}
|
||||
|
||||
func (s *Service) LatestGrafanaVersion() string {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
return s.latestVersion
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
usernameAdmin = "admin"
|
||||
usernameNonAdmin = "nonAdmin"
|
||||
defaultPassword = "password"
|
||||
)
|
||||
|
||||
func TestPluginInstallAccess(t *testing.T) {
|
||||
dir, cfgPath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
PluginAdminEnabled: true,
|
||||
})
|
||||
|
||||
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, cfgPath)
|
||||
store.Bus = bus.GetBus() // in order to allow successful user auth
|
||||
|
||||
createUser(t, store, usernameNonAdmin, defaultPassword, false)
|
||||
createUser(t, store, usernameAdmin, defaultPassword, true)
|
||||
|
||||
t.Run("Request is forbidden if not from an admin", func(t *testing.T) {
|
||||
status, body := makePostRequest(t, grafanaAPIURL(usernameNonAdmin, grafanaListedAddr, "plugins/grafana-plugin/install"))
|
||||
assert.Equal(t, 403, status)
|
||||
assert.Equal(t, "Permission denied", body["message"])
|
||||
|
||||
status, body = makePostRequest(t, grafanaAPIURL(usernameNonAdmin, grafanaListedAddr, "plugins/grafana-plugin/uninstall"))
|
||||
assert.Equal(t, 403, status)
|
||||
assert.Equal(t, "Permission denied", body["message"])
|
||||
})
|
||||
|
||||
t.Run("Request is not forbidden if from an admin", func(t *testing.T) {
|
||||
statusCode, body := makePostRequest(t, grafanaAPIURL(usernameAdmin, grafanaListedAddr, "plugins/test/install"))
|
||||
|
||||
assert.Equal(t, 404, statusCode)
|
||||
assert.Equal(t, "Plugin not found", body["message"])
|
||||
|
||||
statusCode, body = makePostRequest(t, grafanaAPIURL(usernameAdmin, grafanaListedAddr, "plugins/test/uninstall"))
|
||||
assert.Equal(t, 404, statusCode)
|
||||
assert.Equal(t, "Plugin not installed", body["message"])
|
||||
})
|
||||
}
|
||||
|
||||
func createUser(t *testing.T, store *sqlstore.SQLStore, username, password string, isAdmin bool) {
|
||||
t.Helper()
|
||||
|
||||
cmd := models.CreateUserCommand{
|
||||
Login: username,
|
||||
Password: password,
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
_, err := store.CreateUser(context.Background(), cmd)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func makePostRequest(t *testing.T, URL string) (int, map[string]interface{}) {
|
||||
t.Helper()
|
||||
|
||||
// nolint:gosec
|
||||
resp, err := http.Post(URL, "application/json", bytes.NewBufferString(""))
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = resp.Body.Close()
|
||||
log.Warn("Failed to close response body", "err", err)
|
||||
})
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var body = make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &body)
|
||||
require.NoError(t, err)
|
||||
|
||||
return resp.StatusCode, body
|
||||
}
|
||||
|
||||
func grafanaAPIURL(username string, grafanaListedAddr string, path string) string {
|
||||
return fmt.Sprintf("http://%s:%s@%s/api/%s", username, defaultPassword, grafanaListedAddr, path)
|
||||
}
|
136
pkg/tests/api/plugins/api_plugins_test.go
Normal file
136
pkg/tests/api/plugins/api_plugins_test.go
Normal file
@ -0,0 +1,136 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
usernameAdmin = "admin"
|
||||
usernameNonAdmin = "nonAdmin"
|
||||
defaultPassword = "password"
|
||||
)
|
||||
|
||||
func TestPlugins(t *testing.T) {
|
||||
dir, cfgPath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
PluginAdminEnabled: true,
|
||||
})
|
||||
|
||||
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, cfgPath)
|
||||
|
||||
type testCase struct {
|
||||
desc string
|
||||
url string
|
||||
expStatus int
|
||||
expResp string
|
||||
}
|
||||
|
||||
t.Run("Install", func(t *testing.T) {
|
||||
store.Bus = bus.GetBus()
|
||||
|
||||
createUser(t, store, models.CreateUserCommand{Login: usernameNonAdmin, Password: defaultPassword, IsAdmin: false})
|
||||
createUser(t, store, models.CreateUserCommand{Login: usernameAdmin, Password: defaultPassword, IsAdmin: true})
|
||||
|
||||
t.Run("Request is forbidden if not from an admin", func(t *testing.T) {
|
||||
status, body := makePostRequest(t, grafanaAPIURL(usernameNonAdmin, grafanaListedAddr, "plugins/grafana-plugin/install"))
|
||||
assert.Equal(t, 403, status)
|
||||
assert.Equal(t, "Permission denied", body["message"])
|
||||
|
||||
status, body = makePostRequest(t, grafanaAPIURL(usernameNonAdmin, grafanaListedAddr, "plugins/grafana-plugin/uninstall"))
|
||||
assert.Equal(t, 403, status)
|
||||
assert.Equal(t, "Permission denied", body["message"])
|
||||
})
|
||||
|
||||
t.Run("Request is not forbidden if from an admin", func(t *testing.T) {
|
||||
statusCode, body := makePostRequest(t, grafanaAPIURL(usernameAdmin, grafanaListedAddr, "plugins/test/install"))
|
||||
|
||||
assert.Equal(t, 404, statusCode)
|
||||
assert.Equal(t, "Plugin not found", body["message"])
|
||||
|
||||
statusCode, body = makePostRequest(t, grafanaAPIURL(usernameAdmin, grafanaListedAddr, "plugins/test/uninstall"))
|
||||
assert.Equal(t, 404, statusCode)
|
||||
assert.Equal(t, "Plugin not installed", body["message"])
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("List", func(t *testing.T) {
|
||||
testCases := []testCase{
|
||||
{
|
||||
desc: "should return all loaded core and bundled plugins",
|
||||
url: "http://%s/api/plugins",
|
||||
expStatus: http.StatusOK,
|
||||
expResp: expectedResp(t, "expectedListResp.json"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
url := fmt.Sprintf(tc.url, grafanaListedAddr)
|
||||
// nolint:gosec
|
||||
resp, err := http.Get(url)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, resp.Body.Close())
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expStatus, resp.StatusCode)
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, tc.expResp, string(b))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func createUser(t *testing.T, store *sqlstore.SQLStore, cmd models.CreateUserCommand) {
|
||||
t.Helper()
|
||||
|
||||
_, err := store.CreateUser(context.Background(), cmd)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func makePostRequest(t *testing.T, URL string) (int, map[string]interface{}) {
|
||||
t.Helper()
|
||||
|
||||
// nolint:gosec
|
||||
resp, err := http.Post(URL, "application/json", bytes.NewBufferString(""))
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = resp.Body.Close()
|
||||
log.Warn("Failed to close response body", "err", err)
|
||||
})
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
var body = make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &body)
|
||||
require.NoError(t, err)
|
||||
|
||||
return resp.StatusCode, body
|
||||
}
|
||||
|
||||
func grafanaAPIURL(username string, grafanaListedAddr string, path string) string {
|
||||
return fmt.Sprintf("http://%s:%s@%s/api/%s", username, defaultPassword, grafanaListedAddr, path)
|
||||
}
|
||||
|
||||
func expectedResp(t *testing.T, filename string) string {
|
||||
contents, err := ioutil.ReadFile(fmt.Sprintf("data/%s", filename))
|
||||
if err != nil {
|
||||
t.Errorf("failed to load %s: %v", filename, err)
|
||||
}
|
||||
|
||||
return string(contents)
|
||||
}
|
1400
pkg/tests/api/plugins/data/expectedListResp.json
Normal file
1400
pkg/tests/api/plugins/data/expectedListResp.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -157,6 +157,9 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) {
|
||||
provDashboardsDir := filepath.Join(provDir, "dashboards")
|
||||
err = os.MkdirAll(provDashboardsDir, 0750)
|
||||
require.NoError(t, err)
|
||||
corePluginsDir := filepath.Join(publicDir, "app/plugins")
|
||||
err = fs.CopyRecursive(filepath.Join(rootDir, "public", "app/plugins"), corePluginsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := ini.Empty()
|
||||
dfltSect := cfg.Section("")
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user