Plugins: Refactor Plugin Management (#40477)
* add core plugin flow * add instrumentation * move func * remove cruft * support external backend plugins * refactor + clean up * remove comments * refactor loader * simplify core plugin path arg * cleanup loggers * move signature validator to plugins package * fix sig packaging * cleanup plugin model * remove unnecessary plugin field * add start+stop for pm * fix failures * add decommissioned state * export fields just to get things flowing * fix comments * set static routes * make image loading idempotent * merge with backend plugin manager * re-use funcs * reorder imports + remove unnecessary interface * add some TODOs + remove unused func * remove unused instrumentation func * simplify client usage * remove import alias * re-use backendplugin.Plugin interface * re order funcs * improve var name * fix log statements * refactor data model * add logic for dupe check during loading * cleanup state setting * refactor loader * cleanup manager interface * add rendering flow * refactor loading + init * add renderer support * fix renderer plugin * reformat imports * track errors * fix plugin signature inheritance * name param in interface * update func comment * fix func arg name * introduce class concept * remove func * fix external plugin check * apply changes from pm-experiment * fix core plugins * fix imports * rename interface * comment API interface * add support for testdata plugin * enable alerting + use correct core plugin contracts * slim manager API * fix param name * fix filter * support static routes * fix rendering * tidy rendering * get tests compiling * fix install+uninstall * start finder test * add finder test coverage * start loader tests * add test for core plugins * load core + bundled test * add test for nested plugin loading * add test files * clean interface + fix registering some core plugins * refactoring * reformat and create sub packages * simplify core plugin init * fix ctx cancel scenario * migrate initializer * remove Init() funcs * add test starter * new logger * flesh out initializer tests * refactoring * remove unused svc * refactor rendering flow * fixup loader tests * add enabled helper func * fix logger name * fix data fetchers * fix case where plugin dir doesn't exist * improve coverage + move dupe checking to loader * remove noisy debug logs * register core plugins automagically * add support for renderer in catalog * make private func + fix req validation * use interface * re-add check for renderer in catalog * tidy up from moving to auto reg core plugins * core plugin registrar * guards * copy over core plugins for test infra * all tests green * renames * propagate new interfaces * kill old manager * get compiling * tidy up * update naming * refactor manager test + cleanup * add more cases to finder test * migrate validator to field * more coverage * refactor dupe checking * add test for plugin class * add coverage for initializer * split out rendering * move * fixup tests * fix uss test * fix frontend settings * fix grafanads test * add check when checking sig errors * fix enabled map * fixup * allow manual setup of CM * rename to cloud-monitoring * remove TODO * add installer interface for testing * loader interface returns * tests passing * refactor + add more coverage * support 'stackdriver' * fix frontend settings loading * improve naming based on package name * small tidy * refactor test * fix renderer start * make cloud-monitoring plugin ID clearer * add plugin update test * add integration tests * don't break all if sig can't be calculated * add root URL check test * add more signature verification tests * update DTO name * update enabled plugins comment * update comments * fix linter * revert fe naming change * fix errors endpoint * reset error code field name * re-order test to help verify * assert -> require * pm check * add missing entry + re-order * re-check * dump icon log * verify manager contents first * reformat * apply PR feedback * apply style changes * fix one vs all loading err * improve log output * only start when no signature error * move log * rework plugin update check * fix test * fix multi loading from cfg.PluginSettings * improve log output #2 * add error abstraction to capture errors without registering a plugin * add debug log * add unsigned warning * e2e test attempt * fix logger * set home path * prevent panic * alternate * ugh.. fix home path * return renderer even if not started * make renderer plugin managed * add fallback renderer icon, update renderer badge + prevent changes when renderer is installed * fix icon loading * rollback renderer changes * use correct field * remove unneccessary block * remove newline * remove unused func * fix bundled plugins base + module fields * remove unused field since refactor * add authorizer abstraction * loader only returns plugins expected to run * fix multi log output
This commit is contained in:
+1
-1
@@ -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{},
|
||||
}
|
||||
|
||||
+13
-12
@@ -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()
|
||||
|
||||
+32
-32
@@ -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
|
||||
|
||||
+17
-9
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+123
-18
@@ -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"`
|
||||
|
||||
+101
-93
@@ -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")
|
||||
|
||||
+8
-7
@@ -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",
|
||||
|
||||
+48
-9
@@ -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 {
|
||||
|
||||
+84
-44
@@ -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
|
||||
}
|
||||
|
||||
+31
-26
@@ -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]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user