feat(plugins): major improvement in plugins golang code

This commit is contained in:
Torkel Ödegaard 2016-01-09 23:34:20 +01:00
parent 35f40b7312
commit 1ffcea1952
20 changed files with 218 additions and 182 deletions

38
pkg/plugins/app_plugin.go Normal file
View File

@ -0,0 +1,38 @@
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
}

View File

@ -0,0 +1,25 @@
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
}

View File

@ -0,0 +1,47 @@
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)
}

View File

@ -1,15 +1,22 @@
package plugins
import (
"encoding/json"
"github.com/grafana/grafana/pkg/models"
)
type PluginCommon struct {
Type string `json:"type"`
Name string `json:"name"`
Id string `json:"id"`
StaticRoot string `json:"staticRoot"`
Info PluginInfo `json:"info"`
type PluginLoader interface {
Load(decoder *json.Decoder, pluginDir string) error
}
type PluginBase struct {
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 {
@ -29,30 +36,11 @@ type PluginLogos struct {
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 {
Directory string
PluginId string
}
type PanelPlugin struct {
PluginCommon
Module string `json:"module"`
App string `json:"app"`
}
type ApiPluginRoute struct {
Path string `json:"path"`
Method string `json:"method"`
@ -60,34 +48,11 @@ type ApiPluginRoute struct {
ReqGrafanaAdmin bool `json:"reqGrafanaAdmin"`
ReqRole models.RoleType `json:"reqRole"`
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 {
PluginCommon
PluginBase
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 {

View File

@ -0,0 +1,19 @@
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
}

View File

@ -5,10 +5,10 @@ import (
"encoding/json"
"errors"
"io"
"net/url"
"os"
"path"
"path/filepath"
"reflect"
"strings"
"text/template"
@ -24,6 +24,7 @@ var (
ApiPlugins map[string]*ApiPlugin
StaticRoutes []*PluginStaticRoute
Apps map[string]*AppPlugin
PluginTypes map[string]interface{}
)
type PluginScanner struct {
@ -37,6 +38,12 @@ func Init() error {
StaticRoutes = make([]*PluginStaticRoute, 0)
Panels = make(map[string]*PanelPlugin)
Apps = make(map[string]*AppPlugin)
PluginTypes = map[string]interface{}{
"panel": PanelPlugin{},
"datasource": DataSourcePlugin{},
"api": ApiPlugin{},
"app": AppPlugin{},
}
scan(path.Join(setting.StaticRootPath, "app/plugins"))
checkPluginPaths()
@ -115,27 +122,7 @@ func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err erro
return nil
}
func evalRelativePluginUrlPath(pathStr string, pluginId string) string {
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) {
func interpolatePluginJson(reader io.Reader, pluginCommon *PluginBase) (io.Reader, error) {
buf := new(bytes.Buffer)
buf.ReadFrom(reader)
jsonStr := buf.String() //
@ -167,7 +154,7 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
defer reader.Close()
jsonParser := json.NewDecoder(reader)
pluginCommon := PluginCommon{}
pluginCommon := PluginBase{}
if err := jsonParser.Decode(&pluginCommon); err != nil {
return err
}
@ -177,52 +164,21 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
}
reader.Seek(0, 0)
if newReader, err := interpolatePluginJson(reader, &pluginCommon); err != nil {
return err
} else {
jsonParser = json.NewDecoder(newReader)
}
switch pluginCommon.Type {
case "datasource":
p := DataSourcePlugin{}
if err := jsonParser.Decode(&p); err != nil {
return err
}
var loader PluginLoader
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
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:
if pluginGoType, exists := PluginTypes[pluginCommon.Type]; !exists {
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 {

View File

@ -19,6 +19,10 @@ func TestPluginScans(t *testing.T) {
So(err, ShouldBeNil)
So(len(DataSources), 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() {

View File

@ -3,13 +3,6 @@
"name": "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,
"annotations": true
}

View File

@ -3,8 +3,6 @@
"name": "Elasticsearch",
"id": "elasticsearch",
"module": "app/plugins/datasource/elasticsearch/module",
"defaultMatchFormat": "lucene",
"annotations": true,
"metrics": true

View File

@ -3,8 +3,6 @@
"name": "Grafana",
"id": "grafana",
"module": "app/plugins/datasource/grafana/module",
"builtIn": true,
"metrics": true
}

View File

@ -19,6 +19,10 @@ function (angular, GraphiteDatasource) {
return {templateUrl: 'app/plugins/datasource/graphite/partials/annotations.editor.html'};
});
module.directive('datasourceCustomSettingsViewGraphite', function() {
return {templateUrl: 'app/plugins/datasource/graphite/partials/config.html'};
});
return {
Datasource: GraphiteDatasource,
};

View File

@ -3,8 +3,6 @@
"type": "datasource",
"id": "graphite",
"module": "app/plugins/datasource/graphite/module",
"defaultMatchFormat": "glob",
"metrics": true,
"annotations": true

View File

@ -3,8 +3,6 @@
"name": "InfluxDB 0.9.x",
"id": "influxdb",
"module": "app/plugins/datasource/influxdb/module",
"defaultMatchFormat": "regex values",
"metrics": true,
"annotations": true

View File

@ -5,9 +5,5 @@
"builtIn": true,
"mixed": true,
"serviceName": "MixedDatasource",
"module": "app/plugins/datasource/mixed/datasource",
"metrics": true
}

View File

@ -3,8 +3,6 @@
"name": "OpenTSDB",
"id": "opentsdb",
"module": "app/plugins/datasource/opentsdb/module",
"metrics": true,
"defaultMatchFormat": "pipe"
}

View File

@ -0,0 +1,3 @@
declare var Datasource: any;
export default Datasource;

View File

@ -3,31 +3,25 @@ define([
'lodash',
'moment',
'app/core/utils/datemath',
'./directives',
'./query_ctrl',
],
function (angular, _, moment, dateMath) {
'use strict';
var module = angular.module('grafana.services');
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';
this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
this.name = instanceSettings.name;
this.supportMetrics = true;
this.url = instanceSettings.url;
this.directUrl = instanceSettings.directUrl;
this.basicAuth = instanceSettings.basicAuth;
this.withCredentials = instanceSettings.withCredentials;
this.lastErrors = {};
function PrometheusDatasource(datasource) {
this.type = 'prometheus';
this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
this.name = datasource.name;
this.supportMetrics = true;
this.url = datasource.url;
this.directUrl = datasource.directUrl;
this.basicAuth = datasource.basicAuth;
this.withCredentials = datasource.withCredentials;
this.lastErrors = {};
}
PrometheusDatasource.prototype._request = function(method, url) {
this._request = function(method, url) {
var options = {
url: this.url + url,
method: method
@ -46,7 +40,7 @@ function (angular, _, moment, dateMath) {
};
// Called once per panel (graph)
PrometheusDatasource.prototype.query = function(options) {
this.query = function(options) {
var start = getPrometheusTime(options.range.from, false);
var end = getPrometheusTime(options.range.to, true);
@ -86,31 +80,31 @@ function (angular, _, moment, dateMath) {
var self = this;
return $q.all(allQueryPromise)
.then(function(allResponse) {
var result = [];
.then(function(allResponse) {
var result = [];
_.each(allResponse, function(response, index) {
if (response.status === 'error') {
self.lastErrors.query = response.error;
throw response.error;
}
delete self.lastErrors.query;
_.each(allResponse, function(response, index) {
if (response.status === 'error') {
self.lastErrors.query = response.error;
throw response.error;
}
delete self.lastErrors.query;
_.each(response.data.data.result, function(metricData) {
result.push(transformMetricData(metricData, options.targets[index]));
});
_.each(response.data.data.result, function(metricData) {
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;
return this._request('GET', url);
};
PrometheusDatasource.prototype.performSuggestQuery = function(query) {
this.performSuggestQuery = function(query) {
var url = '/api/v1/label/__name__/values';
return this._request('GET', url).then(function(result) {
@ -120,7 +114,7 @@ function (angular, _, moment, dateMath) {
});
};
PrometheusDatasource.prototype.metricFindQuery = function(query) {
this.metricFindQuery = function(query) {
if (!query) { return $q.when([]); }
var interpolated;
@ -196,7 +190,7 @@ function (angular, _, moment, dateMath) {
}
};
PrometheusDatasource.prototype.testDatasource = function() {
this.testDatasource = function() {
return this.metricFindQuery('metrics(.*)').then(function() {
return { status: 'success', message: 'Data source is working', title: 'Success' };
});
@ -276,8 +270,7 @@ function (angular, _, moment, dateMath) {
}
return (date.valueOf() / 1000).toFixed(0);
}
}
return PrometheusDatasource;
});
return PrometheusDatasource;
});

View File

@ -1,7 +1,8 @@
define([
'angular',
'./datasource',
],
function (angular) {
function (angular, PromDatasource) {
'use strict';
var module = angular.module('grafana.directives');
@ -10,4 +11,11 @@ function (angular) {
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
};
});

View File

@ -3,13 +3,5 @@
"name": "Prometheus",
"id": "prometheus",
"serviceName": "PrometheusDatasource",
"module": "app/plugins/datasource/prometheus/datasource",
"partials": {
"config": "app/plugins/datasource/prometheus/partials/config.html"
},
"metrics": true
}

View File

@ -1,17 +1,20 @@
import '../datasource';
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import moment from 'moment';
import helpers from 'test/specs/helpers';
import Datasource from '../datasource';
describe('PrometheusDatasource', function() {
var ctx = new helpers.ServiceTestContext();
var instanceSettings = {url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp' };
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(ctx.createService('PrometheusDatasource'));
beforeEach(function() {
ctx.ds = new ctx.service({ url: 'proxied', directUrl: 'direct', user: 'test', password: 'mupp' });
});
beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
ctx.$q = $q;
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() {
var results;