Commit 1ffcea19 by Torkel Ödegaard

feat(plugins): major improvement in plugins golang code

parent 35f40b73
package plugins
import (
"encoding/json"
"github.com/grafana/grafana/pkg/models"
)
type AppPluginPage struct {
Text string `json:"text"`
Icon string `json:"icon"`
Url string `json:"url"`
ReqRole models.RoleType `json:"reqRole"`
}
type AppPluginCss struct {
Light string `json:"light"`
Dark string `json:"dark"`
}
type AppPlugin struct {
FrontendPluginBase
Enabled bool `json:"enabled"`
Pinned bool `json:"pinned"`
Css *AppPluginCss `json:"css"`
Page *AppPluginPage `json:"page"`
}
func (p *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
if err := decoder.Decode(&p); err != nil {
return err
}
p.PluginDir = pluginDir
p.initFrontendPlugin()
Apps[p.Id] = p
return nil
}
package plugins
import "encoding/json"
type DataSourcePlugin struct {
FrontendPluginBase
DefaultMatchFormat string `json:"defaultMatchFormat"`
Annotations bool `json:"annotations"`
Metrics bool `json:"metrics"`
BuiltIn bool `json:"builtIn"`
Mixed bool `json:"mixed"`
App string `json:"app"`
}
func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
if err := decoder.Decode(&p); err != nil {
return err
}
p.PluginDir = pluginDir
p.initFrontendPlugin()
DataSources[p.Id] = p
return nil
}
package plugins
import (
"net/url"
"path"
)
type FrontendPluginBase struct {
PluginBase
Module string `json:"module"`
StaticRoot string `json:"staticRoot"`
}
func (fp *FrontendPluginBase) initFrontendPlugin() {
if fp.StaticRoot != "" {
StaticRoutes = append(StaticRoutes, &PluginStaticRoute{
Directory: fp.StaticRoot,
PluginId: fp.Id,
})
}
fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.Id)
fp.Info.Logos.Large = evalRelativePluginUrlPath(fp.Info.Logos.Large, fp.Id)
fp.handleModuleDefaults()
}
func (fp *FrontendPluginBase) handleModuleDefaults() {
if fp.Module != "" {
return
}
if fp.StaticRoot != "" {
fp.Module = path.Join("plugins", fp.Type, fp.Id, "module")
return
}
fp.Module = path.Join("app/plugins", fp.Type, fp.Id, "module")
}
func evalRelativePluginUrlPath(pathStr string, pluginId string) string {
u, _ := url.Parse(pathStr)
if u.IsAbs() {
return pathStr
}
return path.Join("public/plugins", pluginId, pathStr)
}
package plugins package plugins
import ( import (
"encoding/json"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
) )
type PluginCommon struct { type PluginLoader interface {
Type string `json:"type"` Load(decoder *json.Decoder, pluginDir string) error
Name string `json:"name"` }
Id string `json:"id"`
StaticRoot string `json:"staticRoot"` type PluginBase struct {
Info PluginInfo `json:"info"` Type string `json:"type"`
Name string `json:"name"`
Id string `json:"id"`
App string `json:"app"`
Info PluginInfo `json:"info"`
PluginDir string `json:"-"`
} }
type PluginInfo struct { type PluginInfo struct {
...@@ -29,30 +36,11 @@ type PluginLogos struct { ...@@ -29,30 +36,11 @@ type PluginLogos struct {
Large string `json:"large"` Large string `json:"large"`
} }
type DataSourcePlugin struct {
PluginCommon
Module string `json:"module"`
ServiceName string `json:"serviceName"`
Partials map[string]interface{} `json:"partials"`
DefaultMatchFormat string `json:"defaultMatchFormat"`
Annotations bool `json:"annotations"`
Metrics bool `json:"metrics"`
BuiltIn bool `json:"builtIn"`
Mixed bool `json:"mixed"`
App string `json:"app"`
}
type PluginStaticRoute struct { type PluginStaticRoute struct {
Directory string Directory string
PluginId string PluginId string
} }
type PanelPlugin struct {
PluginCommon
Module string `json:"module"`
App string `json:"app"`
}
type ApiPluginRoute struct { type ApiPluginRoute struct {
Path string `json:"path"` Path string `json:"path"`
Method string `json:"method"` Method string `json:"method"`
...@@ -60,34 +48,11 @@ type ApiPluginRoute struct { ...@@ -60,34 +48,11 @@ type ApiPluginRoute struct {
ReqGrafanaAdmin bool `json:"reqGrafanaAdmin"` ReqGrafanaAdmin bool `json:"reqGrafanaAdmin"`
ReqRole models.RoleType `json:"reqRole"` ReqRole models.RoleType `json:"reqRole"`
Url string `json:"url"` Url string `json:"url"`
App string `json:"app"`
}
type AppPluginPage struct {
Text string `json:"text"`
Icon string `json:"icon"`
Url string `json:"url"`
ReqRole models.RoleType `json:"reqRole"`
}
type AppPluginCss struct {
Light string `json:"light"`
Dark string `json:"dark"`
} }
type ApiPlugin struct { type ApiPlugin struct {
PluginCommon PluginBase
Routes []*ApiPluginRoute `json:"routes"` Routes []*ApiPluginRoute `json:"routes"`
App string `json:"app"`
}
type AppPlugin struct {
PluginCommon
Enabled bool `json:"enabled"`
Pinned bool `json:"pinned"`
Module string `json:"module"`
Css *AppPluginCss `json:"css"`
Page *AppPluginPage `json:"page"`
} }
type EnabledPlugins struct { type EnabledPlugins struct {
......
package plugins
import "encoding/json"
type PanelPlugin struct {
FrontendPluginBase
}
func (p *PanelPlugin) Load(decoder *json.Decoder, pluginDir string) error {
if err := decoder.Decode(&p); err != nil {
return err
}
p.PluginDir = pluginDir
p.initFrontendPlugin()
Panels[p.Id] = p
return nil
}
...@@ -5,10 +5,10 @@ import ( ...@@ -5,10 +5,10 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"io" "io"
"net/url"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"text/template" "text/template"
...@@ -24,6 +24,7 @@ var ( ...@@ -24,6 +24,7 @@ var (
ApiPlugins map[string]*ApiPlugin ApiPlugins map[string]*ApiPlugin
StaticRoutes []*PluginStaticRoute StaticRoutes []*PluginStaticRoute
Apps map[string]*AppPlugin Apps map[string]*AppPlugin
PluginTypes map[string]interface{}
) )
type PluginScanner struct { type PluginScanner struct {
...@@ -37,6 +38,12 @@ func Init() error { ...@@ -37,6 +38,12 @@ func Init() error {
StaticRoutes = make([]*PluginStaticRoute, 0) StaticRoutes = make([]*PluginStaticRoute, 0)
Panels = make(map[string]*PanelPlugin) Panels = make(map[string]*PanelPlugin)
Apps = make(map[string]*AppPlugin) Apps = make(map[string]*AppPlugin)
PluginTypes = map[string]interface{}{
"panel": PanelPlugin{},
"datasource": DataSourcePlugin{},
"api": ApiPlugin{},
"app": AppPlugin{},
}
scan(path.Join(setting.StaticRootPath, "app/plugins")) scan(path.Join(setting.StaticRootPath, "app/plugins"))
checkPluginPaths() checkPluginPaths()
...@@ -115,27 +122,7 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro ...@@ -115,27 +122,7 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro
return nil return nil
} }
func evalRelativePluginUrlPath(pathStr string, pluginId string) string { func interpolatePluginJson(reader io.Reader, pluginCommon *PluginBase) (io.Reader, error) {
u, _ := url.Parse(pathStr)
if u.IsAbs() {
return pathStr
}
return path.Join("public/plugins", pluginId, pathStr)
}
func addPublicContent(plugin *PluginCommon, currentDir string) {
if plugin.StaticRoot != "" {
StaticRoutes = append(StaticRoutes, &PluginStaticRoute{
Directory: path.Join(currentDir, plugin.StaticRoot),
PluginId: plugin.Id,
})
}
plugin.Info.Logos.Small = evalRelativePluginUrlPath(plugin.Info.Logos.Small, plugin.Id)
plugin.Info.Logos.Large = evalRelativePluginUrlPath(plugin.Info.Logos.Large, plugin.Id)
}
func interpolatePluginJson(reader io.Reader, pluginCommon *PluginCommon) (io.Reader, error) {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
buf.ReadFrom(reader) buf.ReadFrom(reader)
jsonStr := buf.String() // jsonStr := buf.String() //
...@@ -167,7 +154,7 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { ...@@ -167,7 +154,7 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
defer reader.Close() defer reader.Close()
jsonParser := json.NewDecoder(reader) jsonParser := json.NewDecoder(reader)
pluginCommon := PluginCommon{} pluginCommon := PluginBase{}
if err := jsonParser.Decode(&pluginCommon); err != nil { if err := jsonParser.Decode(&pluginCommon); err != nil {
return err return err
} }
...@@ -177,52 +164,21 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error { ...@@ -177,52 +164,21 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
} }
reader.Seek(0, 0) reader.Seek(0, 0)
if newReader, err := interpolatePluginJson(reader, &pluginCommon); err != nil { if newReader, err := interpolatePluginJson(reader, &pluginCommon); err != nil {
return err return err
} else { } else {
jsonParser = json.NewDecoder(newReader) jsonParser = json.NewDecoder(newReader)
} }
switch pluginCommon.Type { var loader PluginLoader
case "datasource":
p := DataSourcePlugin{}
if err := jsonParser.Decode(&p); err != nil {
return err
}
DataSources[p.Id] = &p
addPublicContent(&p.PluginCommon, currentDir)
case "panel":
p := PanelPlugin{}
reader.Seek(0, 0)
if err := jsonParser.Decode(&p); err != nil {
return err
}
Panels[p.Id] = &p if pluginGoType, exists := PluginTypes[pluginCommon.Type]; !exists {
addPublicContent(&p.PluginCommon, currentDir)
case "api":
p := ApiPlugin{}
reader.Seek(0, 0)
if err := jsonParser.Decode(&p); err != nil {
return err
}
ApiPlugins[p.Id] = &p
case "app":
p := AppPlugin{}
reader.Seek(0, 0)
if err := jsonParser.Decode(&p); err != nil {
return err
}
Apps[p.Id] = &p
addPublicContent(&p.PluginCommon, currentDir)
default:
return errors.New("Unkown plugin type " + pluginCommon.Type) return errors.New("Unkown plugin type " + pluginCommon.Type)
} else {
loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader)
} }
return nil return loader.Load(jsonParser, currentDir)
} }
func GetEnabledPlugins(orgApps []*models.AppPlugin) EnabledPlugins { func GetEnabledPlugins(orgApps []*models.AppPlugin) EnabledPlugins {
......
...@@ -19,6 +19,10 @@ func TestPluginScans(t *testing.T) { ...@@ -19,6 +19,10 @@ func TestPluginScans(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(DataSources), ShouldBeGreaterThan, 1) So(len(DataSources), ShouldBeGreaterThan, 1)
So(len(Panels), ShouldBeGreaterThan, 1) So(len(Panels), ShouldBeGreaterThan, 1)
Convey("Should set module automatically", func() {
So(DataSources["graphite"].Module, ShouldEqual, "app/plugins/datasource/graphite/module")
})
}) })
Convey("When reading app plugin definition", t, func() { Convey("When reading app plugin definition", t, func() {
......
...@@ -3,13 +3,6 @@ ...@@ -3,13 +3,6 @@
"name": "CloudWatch", "name": "CloudWatch",
"id": "cloudwatch", "id": "cloudwatch",
"module": "app/plugins/datasource/cloudwatch/module",
"partials": {
"config": "app/plugins/datasource/cloudwatch/partials/config.html",
"query": "app/plugins/datasource/cloudwatch/partials/query.editor.html"
},
"metrics": true, "metrics": true,
"annotations": true "annotations": true
} }
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
"name": "Elasticsearch", "name": "Elasticsearch",
"id": "elasticsearch", "id": "elasticsearch",
"module": "app/plugins/datasource/elasticsearch/module",
"defaultMatchFormat": "lucene", "defaultMatchFormat": "lucene",
"annotations": true, "annotations": true,
"metrics": true "metrics": true
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
"name": "Grafana", "name": "Grafana",
"id": "grafana", "id": "grafana",
"module": "app/plugins/datasource/grafana/module",
"builtIn": true, "builtIn": true,
"metrics": true "metrics": true
} }
...@@ -19,6 +19,10 @@ function (angular, GraphiteDatasource) { ...@@ -19,6 +19,10 @@ function (angular, GraphiteDatasource) {
return {templateUrl: 'app/plugins/datasource/graphite/partials/annotations.editor.html'}; return {templateUrl: 'app/plugins/datasource/graphite/partials/annotations.editor.html'};
}); });
module.directive('datasourceCustomSettingsViewGraphite', function() {
return {templateUrl: 'app/plugins/datasource/graphite/partials/config.html'};
});
return { return {
Datasource: GraphiteDatasource, Datasource: GraphiteDatasource,
}; };
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
"type": "datasource", "type": "datasource",
"id": "graphite", "id": "graphite",
"module": "app/plugins/datasource/graphite/module",
"defaultMatchFormat": "glob", "defaultMatchFormat": "glob",
"metrics": true, "metrics": true,
"annotations": true "annotations": true
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
"name": "InfluxDB 0.9.x", "name": "InfluxDB 0.9.x",
"id": "influxdb", "id": "influxdb",
"module": "app/plugins/datasource/influxdb/module",
"defaultMatchFormat": "regex values", "defaultMatchFormat": "regex values",
"metrics": true, "metrics": true,
"annotations": true "annotations": true
......
...@@ -5,9 +5,5 @@ ...@@ -5,9 +5,5 @@
"builtIn": true, "builtIn": true,
"mixed": true, "mixed": true,
"serviceName": "MixedDatasource",
"module": "app/plugins/datasource/mixed/datasource",
"metrics": true "metrics": true
} }
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
"name": "OpenTSDB", "name": "OpenTSDB",
"id": "opentsdb", "id": "opentsdb",
"module": "app/plugins/datasource/opentsdb/module",
"metrics": true, "metrics": true,
"defaultMatchFormat": "pipe" "defaultMatchFormat": "pipe"
} }
declare var Datasource: any;
export default Datasource;
...@@ -3,31 +3,25 @@ define([ ...@@ -3,31 +3,25 @@ define([
'lodash', 'lodash',
'moment', 'moment',
'app/core/utils/datemath', 'app/core/utils/datemath',
'./directives',
'./query_ctrl', './query_ctrl',
], ],
function (angular, _, moment, dateMath) { function (angular, _, moment, dateMath) {
'use strict'; 'use strict';
var module = angular.module('grafana.services');
var durationSplitRegexp = /(\d+)(ms|s|m|h|d|w|M|y)/; var durationSplitRegexp = /(\d+)(ms|s|m|h|d|w|M|y)/;
module.factory('PrometheusDatasource', function($q, backendSrv, templateSrv) { function PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv) {
this.type = 'prometheus';
function PrometheusDatasource(datasource) { this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
this.type = 'prometheus'; this.name = instanceSettings.name;
this.editorSrc = 'app/features/prometheus/partials/query.editor.html'; this.supportMetrics = true;
this.name = datasource.name; this.url = instanceSettings.url;
this.supportMetrics = true; this.directUrl = instanceSettings.directUrl;
this.url = datasource.url; this.basicAuth = instanceSettings.basicAuth;
this.directUrl = datasource.directUrl; this.withCredentials = instanceSettings.withCredentials;
this.basicAuth = datasource.basicAuth; this.lastErrors = {};
this.withCredentials = datasource.withCredentials;
this.lastErrors = {}; this._request = function(method, url) {
}
PrometheusDatasource.prototype._request = function(method, url) {
var options = { var options = {
url: this.url + url, url: this.url + url,
method: method method: method
...@@ -46,7 +40,7 @@ function (angular, _, moment, dateMath) { ...@@ -46,7 +40,7 @@ function (angular, _, moment, dateMath) {
}; };
// Called once per panel (graph) // Called once per panel (graph)
PrometheusDatasource.prototype.query = function(options) { this.query = function(options) {
var start = getPrometheusTime(options.range.from, false); var start = getPrometheusTime(options.range.from, false);
var end = getPrometheusTime(options.range.to, true); var end = getPrometheusTime(options.range.to, true);
...@@ -86,31 +80,31 @@ function (angular, _, moment, dateMath) { ...@@ -86,31 +80,31 @@ function (angular, _, moment, dateMath) {
var self = this; var self = this;
return $q.all(allQueryPromise) return $q.all(allQueryPromise)
.then(function(allResponse) { .then(function(allResponse) {
var result = []; var result = [];
_.each(allResponse, function(response, index) { _.each(allResponse, function(response, index) {
if (response.status === 'error') { if (response.status === 'error') {
self.lastErrors.query = response.error; self.lastErrors.query = response.error;
throw response.error; throw response.error;
} }
delete self.lastErrors.query; delete self.lastErrors.query;
_.each(response.data.data.result, function(metricData) { _.each(response.data.data.result, function(metricData) {
result.push(transformMetricData(metricData, options.targets[index])); result.push(transformMetricData(metricData, options.targets[index]));
});
}); });
return { data: result };
}); });
return { data: result };
});
}; };
PrometheusDatasource.prototype.performTimeSeriesQuery = function(query, start, end) { this.performTimeSeriesQuery = function(query, start, end) {
var url = '/api/v1/query_range?query=' + encodeURIComponent(query.expr) + '&start=' + start + '&end=' + end + '&step=' + query.step; var url = '/api/v1/query_range?query=' + encodeURIComponent(query.expr) + '&start=' + start + '&end=' + end + '&step=' + query.step;
return this._request('GET', url); return this._request('GET', url);
}; };
PrometheusDatasource.prototype.performSuggestQuery = function(query) { this.performSuggestQuery = function(query) {
var url = '/api/v1/label/__name__/values'; var url = '/api/v1/label/__name__/values';
return this._request('GET', url).then(function(result) { return this._request('GET', url).then(function(result) {
...@@ -120,7 +114,7 @@ function (angular, _, moment, dateMath) { ...@@ -120,7 +114,7 @@ function (angular, _, moment, dateMath) {
}); });
}; };
PrometheusDatasource.prototype.metricFindQuery = function(query) { this.metricFindQuery = function(query) {
if (!query) { return $q.when([]); } if (!query) { return $q.when([]); }
var interpolated; var interpolated;
...@@ -196,7 +190,7 @@ function (angular, _, moment, dateMath) { ...@@ -196,7 +190,7 @@ function (angular, _, moment, dateMath) {
} }
}; };
PrometheusDatasource.prototype.testDatasource = function() { this.testDatasource = function() {
return this.metricFindQuery('metrics(.*)').then(function() { return this.metricFindQuery('metrics(.*)').then(function() {
return { status: 'success', message: 'Data source is working', title: 'Success' }; return { status: 'success', message: 'Data source is working', title: 'Success' };
}); });
...@@ -276,8 +270,7 @@ function (angular, _, moment, dateMath) { ...@@ -276,8 +270,7 @@ function (angular, _, moment, dateMath) {
} }
return (date.valueOf() / 1000).toFixed(0); return (date.valueOf() / 1000).toFixed(0);
} }
}
return PrometheusDatasource; return PrometheusDatasource;
});
}); });
define([ define([
'angular', 'angular',
'./datasource',
], ],
function (angular) { function (angular, PromDatasource) {
'use strict'; 'use strict';
var module = angular.module('grafana.directives'); var module = angular.module('grafana.directives');
...@@ -10,4 +11,11 @@ function (angular) { ...@@ -10,4 +11,11 @@ function (angular) {
return {controller: 'PrometheusQueryCtrl', templateUrl: 'app/plugins/datasource/prometheus/partials/query.editor.html'}; return {controller: 'PrometheusQueryCtrl', templateUrl: 'app/plugins/datasource/prometheus/partials/query.editor.html'};
}); });
module.directive('datasourceCustomSettingsViewPrometheus', function() {
return {templateUrl: 'app/plugins/datasource/prometheus/partials/config.html'};
});
return {
Datasource: PromDatasource
};
}); });
...@@ -3,13 +3,5 @@ ...@@ -3,13 +3,5 @@
"name": "Prometheus", "name": "Prometheus",
"id": "prometheus", "id": "prometheus",
"serviceName": "PrometheusDatasource",
"module": "app/plugins/datasource/prometheus/datasource",
"partials": {
"config": "app/plugins/datasource/prometheus/partials/config.html"
},
"metrics": true "metrics": true
} }
import '../datasource';
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import moment from 'moment'; import moment from 'moment';
import helpers from 'test/specs/helpers'; import helpers from 'test/specs/helpers';
import Datasource from '../datasource';
describe('PrometheusDatasource', function() { describe('PrometheusDatasource', function() {
var ctx = new helpers.ServiceTestContext(); var ctx = new helpers.ServiceTestContext();
var instanceSettings = {url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp' };
beforeEach(angularMocks.module('grafana.core')); beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services')); beforeEach(angularMocks.module('grafana.services'));
beforeEach(ctx.createService('PrometheusDatasource')); beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
beforeEach(function() { ctx.$q = $q;
ctx.ds = new ctx.service({ url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp' }); ctx.$httpBackend = $httpBackend;
}); ctx.$rootScope = $rootScope;
ctx.ds = $injector.instantiate(Datasource, {instanceSettings: instanceSettings});
}));
describe('When querying prometheus with one target using query editor target spec', function() { describe('When querying prometheus with one target using query editor target spec', function() {
var results; var results;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment