Merge branch 'master' into develop-newgrid-row-design2
This commit is contained in:
+3
-3
@@ -222,7 +222,7 @@ func (hs *HttpServer) registerRoutes() {
|
||||
|
||||
r.Get("/plugins", wrap(GetPluginList))
|
||||
r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById))
|
||||
r.Get("/plugins/:pluginId/readme", wrap(GetPluginReadme))
|
||||
r.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown))
|
||||
|
||||
r.Group("/plugins", func() {
|
||||
r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))
|
||||
@@ -230,8 +230,8 @@ func (hs *HttpServer) registerRoutes() {
|
||||
}, reqOrgAdmin)
|
||||
|
||||
r.Get("/frontend/settings/", GetFrontendSettings)
|
||||
r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest)
|
||||
r.Any("/datasources/proxy/:id", reqSignedIn, ProxyDataSourceRequest)
|
||||
r.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest)
|
||||
r.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
|
||||
|
||||
// Dashboard
|
||||
r.Group("/dashboards", func() {
|
||||
|
||||
@@ -32,8 +32,7 @@ func InitAppPluginRoutes(r *macaron.Macaron) {
|
||||
url := util.JoinUrlFragments("/api/plugin-proxy/"+plugin.Id, route.Path)
|
||||
handlers := make([]macaron.Handler, 0)
|
||||
handlers = append(handlers, middleware.Auth(&middleware.AuthOptions{
|
||||
ReqSignedIn: true,
|
||||
ReqGrafanaAdmin: route.ReqGrafanaAdmin,
|
||||
ReqSignedIn: true,
|
||||
}))
|
||||
|
||||
if route.ReqRole != "" {
|
||||
|
||||
@@ -217,7 +217,10 @@ func (this *thunderTask) Fetch() {
|
||||
this.Done()
|
||||
}
|
||||
|
||||
var client = &http.Client{}
|
||||
var client *http.Client = &http.Client{
|
||||
Timeout: time.Second * 2,
|
||||
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
|
||||
}
|
||||
|
||||
func (this *thunderTask) fetch() error {
|
||||
this.Avatar.timestamp = time.Now()
|
||||
|
||||
@@ -91,7 +91,7 @@ func init() {
|
||||
"AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut",
|
||||
"ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"},
|
||||
"AWS/VPN": {"TunnelState", "TunnelDataIn", "TunnelDataOut"},
|
||||
"AWS/WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests"},
|
||||
"WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests"},
|
||||
"AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
|
||||
"KMS": {"SecondsUntilKeyMaterialExpiration"},
|
||||
}
|
||||
@@ -133,7 +133,7 @@ func init() {
|
||||
"AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"},
|
||||
"AWS/SWF": {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
|
||||
"AWS/VPN": {"VpnId", "TunnelIpAddress"},
|
||||
"AWS/WAF": {"Rule", "WebACL"},
|
||||
"WAF": {"Rule", "WebACL"},
|
||||
"AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"},
|
||||
"KMS": {"KeyId"},
|
||||
}
|
||||
@@ -166,9 +166,7 @@ func handleGetNamespaces(req *cwRequest, c *middleware.Context) {
|
||||
|
||||
customNamespaces := req.DataSource.JsonData.Get("customMetricsNamespaces").MustString()
|
||||
if customNamespaces != "" {
|
||||
for _, key := range strings.Split(customNamespaces, ",") {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
keys = append(keys, strings.Split(customNamespaces, ",")...)
|
||||
}
|
||||
|
||||
sort.Sort(sort.StringSlice(keys))
|
||||
@@ -292,11 +290,6 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
|
||||
var metricsCacheLock sync.Mutex
|
||||
|
||||
func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*datasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
|
||||
result, err := getAllMetrics(dsInfo)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
metricsCacheLock.Lock()
|
||||
defer metricsCacheLock.Unlock()
|
||||
|
||||
@@ -314,6 +307,10 @@ func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*data
|
||||
if customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire.After(time.Now()) {
|
||||
return customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil
|
||||
}
|
||||
result, err := getAllMetrics(dsInfo)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0)
|
||||
customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire = time.Now().Add(5 * time.Minute)
|
||||
|
||||
@@ -330,11 +327,6 @@ func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*data
|
||||
var dimensionsCacheLock sync.Mutex
|
||||
|
||||
func getDimensionsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*datasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
|
||||
result, err := getAllMetrics(dsInfo)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
dimensionsCacheLock.Lock()
|
||||
defer dimensionsCacheLock.Unlock()
|
||||
|
||||
@@ -352,6 +344,10 @@ func getDimensionsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*d
|
||||
if customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire.After(time.Now()) {
|
||||
return customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil
|
||||
}
|
||||
result, err := getAllMetrics(dsInfo)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0)
|
||||
customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire = time.Now().Add(5 * time.Minute)
|
||||
|
||||
|
||||
+22
-159
@@ -1,197 +1,60 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/cloudwatch"
|
||||
"github.com/grafana/grafana/pkg/api/pluginproxy"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
)
|
||||
|
||||
var (
|
||||
dataproxyLogger log.Logger = log.New("data-proxy-log")
|
||||
)
|
||||
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
|
||||
|
||||
func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy {
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = targetUrl.Scheme
|
||||
req.URL.Host = targetUrl.Host
|
||||
req.Host = targetUrl.Host
|
||||
func (hs *HttpServer) getDatasourceById(id int64, orgId int64, nocache bool) (*m.DataSource, error) {
|
||||
cacheKey := fmt.Sprintf("ds-%d", id)
|
||||
|
||||
reqQueryVals := req.URL.Query()
|
||||
|
||||
if ds.Type == m.DS_INFLUXDB_08 {
|
||||
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, "db/"+ds.Database+"/"+proxyPath)
|
||||
reqQueryVals.Add("u", ds.User)
|
||||
reqQueryVals.Add("p", ds.Password)
|
||||
req.URL.RawQuery = reqQueryVals.Encode()
|
||||
} else if ds.Type == m.DS_INFLUXDB {
|
||||
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
|
||||
req.URL.RawQuery = reqQueryVals.Encode()
|
||||
if !ds.BasicAuth {
|
||||
req.Header.Del("Authorization")
|
||||
req.Header.Add("Authorization", util.GetBasicAuthHeader(ds.User, ds.Password))
|
||||
}
|
||||
} else {
|
||||
req.URL.Path = util.JoinUrlFragments(targetUrl.Path, proxyPath)
|
||||
}
|
||||
|
||||
if ds.BasicAuth {
|
||||
req.Header.Del("Authorization")
|
||||
req.Header.Add("Authorization", util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword))
|
||||
}
|
||||
|
||||
dsAuth := req.Header.Get("X-DS-Authorization")
|
||||
if len(dsAuth) > 0 {
|
||||
req.Header.Del("X-DS-Authorization")
|
||||
req.Header.Del("Authorization")
|
||||
req.Header.Add("Authorization", dsAuth)
|
||||
}
|
||||
|
||||
// clear cookie headers
|
||||
req.Header.Del("Cookie")
|
||||
req.Header.Del("Set-Cookie")
|
||||
|
||||
// clear X-Forwarded Host/Port/Proto headers
|
||||
req.Header.Del("X-Forwarded-Host")
|
||||
req.Header.Del("X-Forwarded-Port")
|
||||
req.Header.Del("X-Forwarded-Proto")
|
||||
|
||||
// set X-Forwarded-For header
|
||||
if req.RemoteAddr != "" {
|
||||
remoteAddr, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
remoteAddr = req.RemoteAddr
|
||||
}
|
||||
if req.Header.Get("X-Forwarded-For") != "" {
|
||||
req.Header.Set("X-Forwarded-For", req.Header.Get("X-Forwarded-For")+", "+remoteAddr)
|
||||
} else {
|
||||
req.Header.Set("X-Forwarded-For", remoteAddr)
|
||||
if !nocache {
|
||||
if cached, found := hs.cache.Get(cacheKey); found {
|
||||
ds := cached.(*m.DataSource)
|
||||
if ds.OrgId == orgId {
|
||||
return ds, nil
|
||||
}
|
||||
}
|
||||
|
||||
// reqBytes, _ := httputil.DumpRequestOut(req, true);
|
||||
// log.Trace("Proxying datasource request: %s", string(reqBytes))
|
||||
}
|
||||
|
||||
return &httputil.ReverseProxy{Director: director, FlushInterval: time.Millisecond * 200}
|
||||
}
|
||||
|
||||
func getDatasource(id int64, orgId int64) (*m.DataSource, error) {
|
||||
query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgId}
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hs.cache.Set(cacheKey, query.Result, time.Second*5)
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
func ProxyDataSourceRequest(c *middleware.Context) {
|
||||
func (hs *HttpServer) ProxyDataSourceRequest(c *middleware.Context) {
|
||||
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
|
||||
|
||||
ds, err := getDatasource(c.ParamsInt64(":id"), c.OrgId)
|
||||
nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
|
||||
|
||||
ds, err := hs.getDatasourceById(c.ParamsInt64(":id"), c.OrgId, nocache)
|
||||
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to load datasource meta data", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ds.Type == m.DS_INFLUXDB {
|
||||
if c.Query("db") != ds.Database {
|
||||
c.JsonApiErr(403, "Datasource is not configured to allow this database", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ds.Type == m.DS_CLOUDWATCH {
|
||||
cloudwatch.HandleRequest(c, ds)
|
||||
return
|
||||
}
|
||||
|
||||
targetUrl, _ := url.Parse(ds.Url)
|
||||
if !checkWhiteList(c, targetUrl.Host) {
|
||||
// find plugin
|
||||
plugin, ok := plugins.DataSources[ds.Type]
|
||||
if !ok {
|
||||
c.JsonApiErr(500, "Unable to find datasource plugin", err)
|
||||
return
|
||||
}
|
||||
|
||||
proxyPath := c.Params("*")
|
||||
|
||||
if ds.Type == m.DS_PROMETHEUS {
|
||||
if c.Req.Request.Method != http.MethodGet || !strings.HasPrefix(proxyPath, "api/") {
|
||||
c.JsonApiErr(403, "GET is only allowed on proxied Prometheus datasource", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if ds.Type == m.DS_ES {
|
||||
if c.Req.Request.Method == "DELETE" {
|
||||
c.JsonApiErr(403, "Deletes not allowed on proxied Elasticsearch datasource", nil)
|
||||
return
|
||||
}
|
||||
if c.Req.Request.Method == "PUT" {
|
||||
c.JsonApiErr(403, "Puts not allowed on proxied Elasticsearch datasource", nil)
|
||||
return
|
||||
}
|
||||
if c.Req.Request.Method == "POST" && proxyPath != "_msearch" {
|
||||
c.JsonApiErr(403, "Posts not allowed on proxied Elasticsearch datasource except on /_msearch", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
proxy := NewReverseProxy(ds, proxyPath, targetUrl)
|
||||
proxy.Transport, err = ds.GetHttpTransport()
|
||||
if err != nil {
|
||||
c.JsonApiErr(400, "Unable to load TLS certificate", err)
|
||||
return
|
||||
}
|
||||
|
||||
logProxyRequest(ds.Type, c)
|
||||
proxy.ServeHTTP(c.Resp, c.Req.Request)
|
||||
c.Resp.Header().Del("Set-Cookie")
|
||||
}
|
||||
|
||||
func logProxyRequest(dataSourceType string, c *middleware.Context) {
|
||||
if !setting.DataProxyLogging {
|
||||
return
|
||||
}
|
||||
|
||||
var body string
|
||||
if c.Req.Request.Body != nil {
|
||||
buffer, err := ioutil.ReadAll(c.Req.Request.Body)
|
||||
if err == nil {
|
||||
c.Req.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buffer))
|
||||
body = string(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
dataproxyLogger.Info("Proxying incoming request",
|
||||
"userid", c.UserId,
|
||||
"orgid", c.OrgId,
|
||||
"username", c.Login,
|
||||
"datasource", dataSourceType,
|
||||
"uri", c.Req.RequestURI,
|
||||
"method", c.Req.Request.Method,
|
||||
"body", body)
|
||||
}
|
||||
|
||||
func checkWhiteList(c *middleware.Context, host string) bool {
|
||||
if host != "" && len(setting.DataProxyWhiteList) > 0 {
|
||||
if _, exists := setting.DataProxyWhiteList[host]; !exists {
|
||||
c.JsonApiErr(403, "Data proxy hostname and ip are not included in whitelist", nil)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
proxy := pluginproxy.NewDataSourceProxy(ds, plugin, c, proxyPath)
|
||||
proxy.HandleRequest()
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestDataSourceProxy(t *testing.T) {
|
||||
Convey("When getting graphite datasource proxy", t, func() {
|
||||
ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
|
||||
targetUrl, err := url.Parse(ds.Url)
|
||||
proxy := NewReverseProxy(&ds, "/render", targetUrl)
|
||||
proxy.Transport, err = ds.GetHttpTransport()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
transport, ok := proxy.Transport.(*http.Transport)
|
||||
So(ok, ShouldBeTrue)
|
||||
So(transport.TLSClientConfig.InsecureSkipVerify, ShouldBeTrue)
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl}
|
||||
|
||||
proxy.Director(&req)
|
||||
|
||||
Convey("Can translate request url and path", func() {
|
||||
So(req.URL.Host, ShouldEqual, "graphite:8080")
|
||||
So(req.URL.Path, ShouldEqual, "/render")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When getting influxdb datasource proxy", t, func() {
|
||||
ds := m.DataSource{
|
||||
Type: m.DS_INFLUXDB_08,
|
||||
Url: "http://influxdb:8083",
|
||||
Database: "site",
|
||||
User: "user",
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
targetUrl, _ := url.Parse(ds.Url)
|
||||
proxy := NewReverseProxy(&ds, "", targetUrl)
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl}
|
||||
|
||||
proxy.Director(&req)
|
||||
|
||||
Convey("Should add db to url", func() {
|
||||
So(req.URL.Path, ShouldEqual, "/db/site/")
|
||||
})
|
||||
|
||||
Convey("Should add username and password", func() {
|
||||
queryVals := req.URL.Query()
|
||||
So(queryVals["u"][0], ShouldEqual, "user")
|
||||
So(queryVals["p"][0], ShouldEqual, "password")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
gocache "github.com/patrickmn/go-cache"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/live"
|
||||
@@ -29,13 +31,15 @@ type HttpServer struct {
|
||||
macaron *macaron.Macaron
|
||||
context context.Context
|
||||
streamManager *live.StreamManager
|
||||
cache *gocache.Cache
|
||||
|
||||
httpSrv *http.Server
|
||||
}
|
||||
|
||||
func NewHttpServer() *HttpServer {
|
||||
return &HttpServer{
|
||||
log: log.New("http.server"),
|
||||
log: log.New("http.server"),
|
||||
cache: gocache.New(5*time.Minute, 10*time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/cloudwatch"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var (
|
||||
logger log.Logger = log.New("data-proxy-log")
|
||||
client *http.Client = &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
|
||||
}
|
||||
tokenCache = map[int64]*jwtToken{}
|
||||
)
|
||||
|
||||
type jwtToken struct {
|
||||
ExpiresOn time.Time `json:"-"`
|
||||
ExpiresOnString string `json:"expires_on"`
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
type DataSourceProxy struct {
|
||||
ds *m.DataSource
|
||||
ctx *middleware.Context
|
||||
targetUrl *url.URL
|
||||
proxyPath string
|
||||
route *plugins.AppPluginRoute
|
||||
plugin *plugins.DataSourcePlugin
|
||||
}
|
||||
|
||||
func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *middleware.Context, proxyPath string) *DataSourceProxy {
|
||||
targetUrl, _ := url.Parse(ds.Url)
|
||||
|
||||
return &DataSourceProxy{
|
||||
ds: ds,
|
||||
plugin: plugin,
|
||||
ctx: ctx,
|
||||
proxyPath: proxyPath,
|
||||
targetUrl: targetUrl,
|
||||
}
|
||||
}
|
||||
|
||||
func (proxy *DataSourceProxy) HandleRequest() {
|
||||
if proxy.ds.Type == m.DS_CLOUDWATCH {
|
||||
cloudwatch.HandleRequest(proxy.ctx, proxy.ds)
|
||||
return
|
||||
}
|
||||
|
||||
if err := proxy.validateRequest(); err != nil {
|
||||
proxy.ctx.JsonApiErr(403, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
reverseProxy := &httputil.ReverseProxy{
|
||||
Director: proxy.getDirector(),
|
||||
FlushInterval: time.Millisecond * 200,
|
||||
}
|
||||
|
||||
var err error
|
||||
reverseProxy.Transport, err = proxy.ds.GetHttpTransport()
|
||||
if err != nil {
|
||||
proxy.ctx.JsonApiErr(400, "Unable to load TLS certificate", err)
|
||||
return
|
||||
}
|
||||
|
||||
proxy.logRequest()
|
||||
|
||||
reverseProxy.ServeHTTP(proxy.ctx.Resp, proxy.ctx.Req.Request)
|
||||
proxy.ctx.Resp.Header().Del("Set-Cookie")
|
||||
}
|
||||
|
||||
func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
|
||||
return func(req *http.Request) {
|
||||
req.URL.Scheme = proxy.targetUrl.Scheme
|
||||
req.URL.Host = proxy.targetUrl.Host
|
||||
req.Host = proxy.targetUrl.Host
|
||||
|
||||
reqQueryVals := req.URL.Query()
|
||||
|
||||
if proxy.ds.Type == m.DS_INFLUXDB_08 {
|
||||
req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, "db/"+proxy.ds.Database+"/"+proxy.proxyPath)
|
||||
reqQueryVals.Add("u", proxy.ds.User)
|
||||
reqQueryVals.Add("p", proxy.ds.Password)
|
||||
req.URL.RawQuery = reqQueryVals.Encode()
|
||||
} else if proxy.ds.Type == m.DS_INFLUXDB {
|
||||
req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, proxy.proxyPath)
|
||||
req.URL.RawQuery = reqQueryVals.Encode()
|
||||
if !proxy.ds.BasicAuth {
|
||||
req.Header.Del("Authorization")
|
||||
req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.User, proxy.ds.Password))
|
||||
}
|
||||
} else {
|
||||
req.URL.Path = util.JoinUrlFragments(proxy.targetUrl.Path, proxy.proxyPath)
|
||||
}
|
||||
|
||||
if proxy.ds.BasicAuth {
|
||||
req.Header.Del("Authorization")
|
||||
req.Header.Add("Authorization", util.GetBasicAuthHeader(proxy.ds.BasicAuthUser, proxy.ds.BasicAuthPassword))
|
||||
}
|
||||
|
||||
dsAuth := req.Header.Get("X-DS-Authorization")
|
||||
if len(dsAuth) > 0 {
|
||||
req.Header.Del("X-DS-Authorization")
|
||||
req.Header.Del("Authorization")
|
||||
req.Header.Add("Authorization", dsAuth)
|
||||
}
|
||||
|
||||
// clear cookie headers
|
||||
req.Header.Del("Cookie")
|
||||
req.Header.Del("Set-Cookie")
|
||||
|
||||
// clear X-Forwarded Host/Port/Proto headers
|
||||
req.Header.Del("X-Forwarded-Host")
|
||||
req.Header.Del("X-Forwarded-Port")
|
||||
req.Header.Del("X-Forwarded-Proto")
|
||||
|
||||
// set X-Forwarded-For header
|
||||
if req.RemoteAddr != "" {
|
||||
remoteAddr, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
if err != nil {
|
||||
remoteAddr = req.RemoteAddr
|
||||
}
|
||||
if req.Header.Get("X-Forwarded-For") != "" {
|
||||
req.Header.Set("X-Forwarded-For", req.Header.Get("X-Forwarded-For")+", "+remoteAddr)
|
||||
} else {
|
||||
req.Header.Set("X-Forwarded-For", remoteAddr)
|
||||
}
|
||||
}
|
||||
|
||||
if proxy.route != nil {
|
||||
proxy.applyRoute(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (proxy *DataSourceProxy) validateRequest() error {
|
||||
if proxy.ds.Type == m.DS_INFLUXDB {
|
||||
if proxy.ctx.Query("db") != proxy.ds.Database {
|
||||
return errors.New("Datasource is not configured to allow this database")
|
||||
}
|
||||
}
|
||||
|
||||
if !checkWhiteList(proxy.ctx, proxy.targetUrl.Host) {
|
||||
return errors.New("Target url is not a valid target")
|
||||
}
|
||||
|
||||
if proxy.ds.Type == m.DS_PROMETHEUS {
|
||||
if proxy.ctx.Req.Request.Method != http.MethodGet || !strings.HasPrefix(proxy.proxyPath, "api/") {
|
||||
return errors.New("GET is only allowed on proxied Prometheus datasource")
|
||||
}
|
||||
}
|
||||
|
||||
if proxy.ds.Type == m.DS_ES {
|
||||
if proxy.ctx.Req.Request.Method == "DELETE" {
|
||||
return errors.New("Deletes not allowed on proxied Elasticsearch datasource")
|
||||
}
|
||||
if proxy.ctx.Req.Request.Method == "PUT" {
|
||||
return errors.New("Puts not allowed on proxied Elasticsearch datasource")
|
||||
}
|
||||
if proxy.ctx.Req.Request.Method == "POST" && proxy.proxyPath != "_msearch" {
|
||||
return errors.New("Posts not allowed on proxied Elasticsearch datasource except on /_msearch")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if route.ReqRole.IsValid() {
|
||||
if !proxy.ctx.HasUserRole(route.ReqRole) {
|
||||
return errors.New("Plugin proxy route access denied")
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(proxy.proxyPath, route.Path) {
|
||||
proxy.route = route
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (proxy *DataSourceProxy) logRequest() {
|
||||
if !setting.DataProxyLogging {
|
||||
return
|
||||
}
|
||||
|
||||
var body string
|
||||
if proxy.ctx.Req.Request.Body != nil {
|
||||
buffer, err := ioutil.ReadAll(proxy.ctx.Req.Request.Body)
|
||||
if err == nil {
|
||||
proxy.ctx.Req.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buffer))
|
||||
body = string(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Proxying incoming request",
|
||||
"userid", proxy.ctx.UserId,
|
||||
"orgid", proxy.ctx.OrgId,
|
||||
"username", proxy.ctx.Login,
|
||||
"datasource", proxy.ds.Type,
|
||||
"uri", proxy.ctx.Req.RequestURI,
|
||||
"method", proxy.ctx.Req.Request.Method,
|
||||
"body", body)
|
||||
}
|
||||
|
||||
func checkWhiteList(c *middleware.Context, host string) bool {
|
||||
if host != "" && len(setting.DataProxyWhiteList) > 0 {
|
||||
if _, exists := setting.DataProxyWhiteList[host]; !exists {
|
||||
c.JsonApiErr(403, "Data proxy hostname and ip are not included in whitelist", nil)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
|
||||
proxy.proxyPath = strings.TrimPrefix(proxy.proxyPath, proxy.route.Path)
|
||||
|
||||
data := templateData{
|
||||
JsonData: proxy.ds.JsonData.Interface().(map[string]interface{}),
|
||||
SecureJsonData: proxy.ds.SecureJsonData.Decrypt(),
|
||||
}
|
||||
|
||||
routeUrl, err := url.Parse(proxy.route.Url)
|
||||
if err != nil {
|
||||
logger.Error("Error parsing plugin route url")
|
||||
return
|
||||
}
|
||||
|
||||
req.URL.Scheme = routeUrl.Scheme
|
||||
req.URL.Host = routeUrl.Host
|
||||
req.Host = routeUrl.Host
|
||||
req.URL.Path = util.JoinUrlFragments(routeUrl.Path, proxy.proxyPath)
|
||||
|
||||
if err := addHeaders(&req.Header, proxy.route, data); err != nil {
|
||||
logger.Error("Failed to render plugin headers", "error", err)
|
||||
}
|
||||
|
||||
if proxy.route.TokenAuth != nil {
|
||||
if token, err := proxy.getAccessToken(data); err != nil {
|
||||
logger.Error("Failed to get access token", "error", err)
|
||||
} else {
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Requesting", "url", req.URL.String())
|
||||
}
|
||||
|
||||
func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error) {
|
||||
if cachedToken, found := tokenCache[proxy.ds.Id]; found {
|
||||
if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) {
|
||||
logger.Info("Using token from cache")
|
||||
return cachedToken.AccessToken, nil
|
||||
}
|
||||
}
|
||||
|
||||
urlInterpolated, err := interpolateString(proxy.route.TokenAuth.Url, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
params := make(url.Values)
|
||||
for key, value := range proxy.route.TokenAuth.Params {
|
||||
if interpolatedParam, err := interpolateString(value, data); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
params.Add(key, interpolatedParam)
|
||||
}
|
||||
}
|
||||
|
||||
getTokenReq, _ := http.NewRequest("POST", urlInterpolated, bytes.NewBufferString(params.Encode()))
|
||||
getTokenReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
getTokenReq.Header.Add("Content-Length", strconv.Itoa(len(params.Encode())))
|
||||
|
||||
resp, err := client.Do(getTokenReq)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
var token jwtToken
|
||||
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64)
|
||||
token.ExpiresOn = time.Unix(expiresOnEpoch, 0)
|
||||
tokenCache[proxy.ds.Id] = &token
|
||||
|
||||
logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn)
|
||||
return token.AccessToken, nil
|
||||
}
|
||||
|
||||
func interpolateString(text string, data templateData) (string, error) {
|
||||
t, err := template.New("content").Parse(text)
|
||||
if err != nil {
|
||||
return "", errors.New(fmt.Sprintf("Could not parse template %s.", text))
|
||||
}
|
||||
|
||||
var contentBuf bytes.Buffer
|
||||
err = t.Execute(&contentBuf, data)
|
||||
if err != nil {
|
||||
return "", errors.New(fmt.Sprintf("Failed to execute template %s.", text))
|
||||
}
|
||||
|
||||
return contentBuf.String(), nil
|
||||
}
|
||||
|
||||
func addHeaders(reqHeaders *http.Header, route *plugins.AppPluginRoute, data templateData) error {
|
||||
for _, header := range route.Headers {
|
||||
interpolated, err := interpolateString(header.Content, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqHeaders.Add(header.Name, interpolated)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDSRouteRule(t *testing.T) {
|
||||
|
||||
Convey("DataSourceProxy", t, func() {
|
||||
Convey("Plugin with routes", func() {
|
||||
plugin := &plugins.DataSourcePlugin{
|
||||
Routes: []*plugins.AppPluginRoute{
|
||||
{
|
||||
Path: "api/v4/",
|
||||
Url: "https://www.google.com",
|
||||
ReqRole: m.ROLE_EDITOR,
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "api/admin",
|
||||
Url: "https://www.google.com",
|
||||
ReqRole: m.ROLE_ADMIN,
|
||||
Headers: []plugins.AppPluginRouteHeader{
|
||||
{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}}"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
setting.SecretKey = "password"
|
||||
key, _ := util.Encrypt([]byte("123"), "password")
|
||||
|
||||
ds := &m.DataSource{
|
||||
JsonData: simplejson.NewFromAny(map[string]interface{}{
|
||||
"clientId": "asd",
|
||||
}),
|
||||
SecureJsonData: map[string][]byte{
|
||||
"key": key,
|
||||
},
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
|
||||
ctx := &middleware.Context{
|
||||
Context: &macaron.Context{
|
||||
Req: macaron.Request{Request: req},
|
||||
},
|
||||
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR},
|
||||
}
|
||||
|
||||
Convey("When matching route path", func() {
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
|
||||
proxy.route = plugin.Routes[0]
|
||||
proxy.applyRoute(req)
|
||||
|
||||
Convey("should add headers and update url", func() {
|
||||
So(req.URL.String(), ShouldEqual, "https://www.google.com/some/method")
|
||||
So(req.Header.Get("x-header"), ShouldEqual, "my secret 123")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Validating request", func() {
|
||||
Convey("plugin route with valid role", func() {
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method")
|
||||
err := proxy.validateRequest()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("plugin route with admin role and user is editor", func() {
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin")
|
||||
err := proxy.validateRequest()
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("plugin route with admin role and user is admin", func() {
|
||||
ctx.SignedInUser.OrgRole = m.ROLE_ADMIN
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "api/admin")
|
||||
err := proxy.validateRequest()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When proxying graphite", func() {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
|
||||
ctx := &middleware.Context{}
|
||||
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "/render")
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl}
|
||||
|
||||
proxy.getDirector()(&req)
|
||||
|
||||
Convey("Can translate request url and path", func() {
|
||||
So(req.URL.Host, ShouldEqual, "graphite:8080")
|
||||
So(req.URL.Path, ShouldEqual, "/render")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When proxying InfluxDB", func() {
|
||||
plugin := &plugins.DataSourcePlugin{}
|
||||
|
||||
ds := &m.DataSource{
|
||||
Type: m.DS_INFLUXDB_08,
|
||||
Url: "http://influxdb:8083",
|
||||
Database: "site",
|
||||
User: "user",
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
ctx := &middleware.Context{}
|
||||
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
|
||||
|
||||
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||
req := http.Request{URL: requestUrl}
|
||||
|
||||
proxy.getDirector()(&req)
|
||||
|
||||
Convey("Should add db to url", func() {
|
||||
So(req.URL.Path, ShouldEqual, "/db/site/")
|
||||
})
|
||||
|
||||
Convey("Should add username and password", func() {
|
||||
queryVals := req.URL.Query()
|
||||
So(queryVals["u"][0], ShouldEqual, "user")
|
||||
So(queryVals["p"][0], ShouldEqual, "password")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When interpolating string", func() {
|
||||
data := templateData{
|
||||
SecureJsonData: map[string]string{
|
||||
"Test": "0asd+asd",
|
||||
},
|
||||
}
|
||||
|
||||
interpolated, err := interpolateString("{{.SecureJsonData.Test}}", data)
|
||||
So(err, ShouldBeNil)
|
||||
So(interpolated, ShouldEqual, "0asd+asd")
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
package pluginproxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"text/template"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
@@ -38,23 +34,8 @@ func getHeaders(route *plugins.AppPluginRoute, orgId int64, appId string) (http.
|
||||
SecureJsonData: query.Result.SecureJsonData.Decrypt(),
|
||||
}
|
||||
|
||||
for _, header := range route.Headers {
|
||||
var contentBuf bytes.Buffer
|
||||
t, err := template.New("content").Parse(header.Content)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("could not parse header content template for header %s.", header.Name))
|
||||
}
|
||||
|
||||
err = t.Execute(&contentBuf, data)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("failed to execute header content template for header %s.", header.Name))
|
||||
}
|
||||
|
||||
log.Trace("Adding header to proxy request. %s: %s", header.Name, contentBuf.String())
|
||||
result.Add(header.Name, contentBuf.String())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
err := addHeaders(&result, route, data)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins.AppPluginRoute, appId string) *httputil.ReverseProxy {
|
||||
|
||||
+4
-3
@@ -147,15 +147,16 @@ func GetPluginDashboards(c *middleware.Context) Response {
|
||||
}
|
||||
}
|
||||
|
||||
func GetPluginReadme(c *middleware.Context) Response {
|
||||
func GetPluginMarkdown(c *middleware.Context) Response {
|
||||
pluginId := c.Params(":pluginId")
|
||||
name := c.Params(":name")
|
||||
|
||||
if content, err := plugins.GetPluginReadme(pluginId); err != nil {
|
||||
if content, err := plugins.GetPluginMarkdown(pluginId, name); err != nil {
|
||||
if notfound, ok := err.(plugins.PluginNotFoundError); ok {
|
||||
return ApiError(404, notfound.Error(), nil)
|
||||
}
|
||||
|
||||
return ApiError(500, "Could not get readme", err)
|
||||
return ApiError(500, "Could not get markdown file", err)
|
||||
} else {
|
||||
return Respond(200, content)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,8 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/trace"
|
||||
"strconv"
|
||||
@@ -16,7 +14,6 @@ import (
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -87,46 +84,11 @@ func main() {
|
||||
server.Start()
|
||||
}
|
||||
|
||||
func initRuntime() {
|
||||
err := setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
Config: *configFile,
|
||||
HomePath: *homePath,
|
||||
Args: flag.Args(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(3, err.Error())
|
||||
}
|
||||
|
||||
logger := log.New("main")
|
||||
logger.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
|
||||
|
||||
setting.LogConfigurationInfo()
|
||||
}
|
||||
|
||||
func initSql() {
|
||||
sqlstore.NewEngine()
|
||||
sqlstore.EnsureAdminUser()
|
||||
}
|
||||
|
||||
func writePIDFile() {
|
||||
if *pidFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the required directory structure exists.
|
||||
err := os.MkdirAll(filepath.Dir(*pidFile), 0700)
|
||||
if err != nil {
|
||||
log.Fatal(3, "Failed to verify pid directory", err)
|
||||
}
|
||||
|
||||
// Retrieve the PID and write it.
|
||||
pid := strconv.Itoa(os.Getpid())
|
||||
if err := ioutil.WriteFile(*pidFile, []byte(pid), 0644); err != nil {
|
||||
log.Fatal(3, "Failed to write pidfile", err)
|
||||
}
|
||||
}
|
||||
|
||||
func listenToSystemSignals(server models.GrafanaServer) {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
ignoreChan := make(chan os.Signal, 1)
|
||||
|
||||
@@ -2,7 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
@@ -45,8 +50,9 @@ type GrafanaServerImpl struct {
|
||||
func (g *GrafanaServerImpl) Start() {
|
||||
go listenToSystemSignals(g)
|
||||
|
||||
writePIDFile()
|
||||
initRuntime()
|
||||
g.initLogging()
|
||||
g.writePIDFile()
|
||||
|
||||
initSql()
|
||||
metrics.Init()
|
||||
search.Init()
|
||||
@@ -74,6 +80,22 @@ func (g *GrafanaServerImpl) Start() {
|
||||
g.startHttpServer()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) initLogging() {
|
||||
err := setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
Config: *configFile,
|
||||
HomePath: *homePath,
|
||||
Args: flag.Args(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
g.log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
g.log.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
|
||||
setting.LogConfigurationInfo()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) startHttpServer() {
|
||||
g.httpServer = api.NewHttpServer()
|
||||
|
||||
@@ -101,3 +123,25 @@ func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
|
||||
log.Close()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) writePIDFile() {
|
||||
if *pidFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the required directory structure exists.
|
||||
err := os.MkdirAll(filepath.Dir(*pidFile), 0700)
|
||||
if err != nil {
|
||||
g.log.Error("Failed to verify pid directory", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Retrieve the PID and write it.
|
||||
pid := strconv.Itoa(os.Getpid())
|
||||
if err := ioutil.WriteFile(*pidFile, []byte(pid), 0644); err != nil {
|
||||
g.log.Error("Failed to write pidfile", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
g.log.Info("Writing PID file", "path", *pidFile, "pid", pid)
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
|
||||
// in the JSON document to a top level key.
|
||||
//
|
||||
// In order to produce distinct "blocks" when rendering the basic diff,
|
||||
// we need a way to distinguish between differnt sections of data.
|
||||
// we need a way to distinguish between different sections of data.
|
||||
// To do this, we consider the value(s) of each top-level JSON key to
|
||||
// represent a distinct block for Grafana's JSON data structure, so
|
||||
// we perform this check to see if we've entered a new "block". If we
|
||||
|
||||
@@ -302,16 +302,16 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, postion diff.Position) (results []diff.Delta) {
|
||||
func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, position diff.Position) (results []diff.Delta) {
|
||||
results = make([]diff.Delta, 0)
|
||||
for _, delta := range deltas {
|
||||
switch delta.(type) {
|
||||
case diff.PostDelta:
|
||||
if delta.(diff.PostDelta).PostPosition() == postion {
|
||||
if delta.(diff.PostDelta).PostPosition() == position {
|
||||
results = append(results, delta)
|
||||
}
|
||||
case diff.PreDelta:
|
||||
if delta.(diff.PreDelta).PrePosition() == postion {
|
||||
if delta.(diff.PreDelta).PrePosition() == position {
|
||||
results = append(results, delta)
|
||||
}
|
||||
default:
|
||||
|
||||
+2
-2
@@ -33,8 +33,8 @@ func AuthenticateUser(query *LoginUserQuery) error {
|
||||
|
||||
if setting.LdapEnabled {
|
||||
for _, server := range LdapCfg.Servers {
|
||||
auther := NewLdapAuthenticator(server)
|
||||
err = auther.Login(query)
|
||||
author := NewLdapAuthenticator(server)
|
||||
err = author.Login(query)
|
||||
if err == nil || err != ErrInvalidCredentials {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ var (
|
||||
M_Alerting_Notification_Sent_Pushover Counter
|
||||
M_Aws_CloudWatch_GetMetricStatistics Counter
|
||||
M_Aws_CloudWatch_ListMetrics Counter
|
||||
M_DB_DataSource_QueryById Counter
|
||||
|
||||
// Timers
|
||||
M_DataSource_ProxyReq_Timer Timer
|
||||
@@ -135,6 +136,8 @@ func initMetricVars(settings *MetricSettings) {
|
||||
M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics")
|
||||
M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics")
|
||||
|
||||
M_DB_DataSource_QueryById = RegCounter("db.datasource.query_by_id")
|
||||
|
||||
// Timers
|
||||
M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all")
|
||||
M_Alerting_Execution_Time = RegTimer("alerting.execution_time")
|
||||
|
||||
@@ -67,7 +67,7 @@ func initContextWithAuthProxy(ctx *Context, orgId int64) bool {
|
||||
if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId {
|
||||
// remove session
|
||||
if err := ctx.Session.Destory(ctx); err != nil {
|
||||
log.Error(3, "Failed to destory session, err")
|
||||
log.Error(3, "Failed to destroy session, err")
|
||||
}
|
||||
|
||||
// initialize a new session
|
||||
@@ -107,8 +107,8 @@ var syncGrafanaUserWithLdapUser = func(ctx *Context, query *m.GetSignedInUserQue
|
||||
ldapCfg := login.LdapCfg
|
||||
|
||||
for _, server := range ldapCfg.Servers {
|
||||
auther := login.NewLdapAuthenticator(server)
|
||||
if err := auther.SyncSignedInUser(query.Result); err != nil {
|
||||
author := login.NewLdapAuthenticator(server)
|
||||
if err := author.SyncSignedInUser(query.Result); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ type AppPlugin struct {
|
||||
}
|
||||
|
||||
type AppPluginRoute struct {
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
ReqGrafanaAdmin bool `json:"reqGrafanaAdmin"`
|
||||
ReqRole models.RoleType `json:"reqRole"`
|
||||
Url string `json:"url"`
|
||||
Headers []AppPluginRouteHeader `json:"headers"`
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
ReqRole models.RoleType `json:"reqRole"`
|
||||
Url string `json:"url"`
|
||||
Headers []AppPluginRouteHeader `json:"headers"`
|
||||
TokenAuth *JwtTokenAuth `json:"tokenAuth"`
|
||||
}
|
||||
|
||||
type AppPluginRouteHeader struct {
|
||||
@@ -36,6 +36,11 @@ type AppPluginRouteHeader struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type JwtTokenAuth struct {
|
||||
Url string `json:"url"`
|
||||
Params map[string]string `json:"params"`
|
||||
}
|
||||
|
||||
func (app *AppPlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
if err := decoder.Decode(&app); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
package plugins
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type DataSourcePlugin struct {
|
||||
FrontendPluginBase
|
||||
Annotations bool `json:"annotations"`
|
||||
Metrics bool `json:"metrics"`
|
||||
Alerting bool `json:"alerting"`
|
||||
BuiltIn bool `json:"builtIn"`
|
||||
Mixed bool `json:"mixed"`
|
||||
App string `json:"app"`
|
||||
Annotations bool `json:"annotations"`
|
||||
Metrics bool `json:"metrics"`
|
||||
Alerting bool `json:"alerting"`
|
||||
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
|
||||
BuiltIn bool `json:"builtIn,omitempty"`
|
||||
Mixed bool `json:"mixed,omitempty"`
|
||||
HasQueryHelp bool `json:"hasQueryHelp,omitempty"`
|
||||
Routes []*AppPluginRoute `json:"routes"`
|
||||
}
|
||||
|
||||
func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
@@ -21,6 +27,15 @@ func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// look for help markdown
|
||||
helpPath := filepath.Join(p.PluginDir, "QUERY_HELP.md")
|
||||
if _, err := os.Stat(helpPath); os.IsNotExist(err) {
|
||||
helpPath = filepath.Join(p.PluginDir, "query_help.md")
|
||||
}
|
||||
if _, err := os.Stat(helpPath); err == nil {
|
||||
p.HasQueryHelp = true
|
||||
}
|
||||
|
||||
DataSources[p.Id] = p
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ type PluginBase struct {
|
||||
Includes []*PluginInclude `json:"includes"`
|
||||
Module string `json:"module"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
HideFromList bool `json:"hideFromList"`
|
||||
State string `json:"state"`
|
||||
HideFromList bool `json:"hideFromList,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
|
||||
IncludedInAppId string `json:"-"`
|
||||
PluginDir string `json:"-"`
|
||||
@@ -48,9 +48,6 @@ type PluginBase struct {
|
||||
|
||||
GrafanaNetVersion string `json:"-"`
|
||||
GrafanaNetHasUpdate bool `json:"-"`
|
||||
|
||||
// cache for readme file contents
|
||||
Readme []byte `json:"-"`
|
||||
}
|
||||
|
||||
func (pb *PluginBase) registerPlugin(pluginDir string) error {
|
||||
|
||||
+9
-14
@@ -3,6 +3,7 @@ package plugins
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
@@ -166,30 +167,24 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
|
||||
return loader.Load(jsonParser, currentDir)
|
||||
}
|
||||
|
||||
func GetPluginReadme(pluginId string) ([]byte, error) {
|
||||
func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
|
||||
plug, exists := Plugins[pluginId]
|
||||
if !exists {
|
||||
return nil, PluginNotFoundError{pluginId}
|
||||
}
|
||||
|
||||
if plug.Readme != nil {
|
||||
return plug.Readme, nil
|
||||
path := filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
path = filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name)))
|
||||
}
|
||||
|
||||
readmePath := filepath.Join(plug.PluginDir, "README.md")
|
||||
if _, err := os.Stat(readmePath); os.IsNotExist(err) {
|
||||
readmePath = filepath.Join(plug.PluginDir, "readme.md")
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return make([]byte, 0), nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(readmePath); os.IsNotExist(err) {
|
||||
plug.Readme = make([]byte, 0)
|
||||
return plug.Readme, nil
|
||||
}
|
||||
|
||||
if readmeBytes, err := ioutil.ReadFile(readmePath); err != nil {
|
||||
if data, err := ioutil.ReadFile(path); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
plug.Readme = readmeBytes
|
||||
return plug.Readme, nil
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange *
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(getDsInfo); err != nil {
|
||||
return nil, fmt.Errorf("Could not find datasource")
|
||||
return nil, fmt.Errorf("Could not find datasource %v", err)
|
||||
}
|
||||
|
||||
req := c.getRequestForAlertRule(getDsInfo.Result, timeRange)
|
||||
|
||||
@@ -84,7 +84,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
message := evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "<br><a href=" + ruleUrl + ">Check Dasboard</a>"
|
||||
message := evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "<br><a href=" + ruleUrl + ">Check Dashboard</a>"
|
||||
fields := make([]map[string]interface{}, 0)
|
||||
message += "<br>"
|
||||
for index, evt := range evalContext.EvalMatches {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/securejsondata"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
@@ -19,6 +20,8 @@ func init() {
|
||||
}
|
||||
|
||||
func GetDataSourceById(query *m.GetDataSourceByIdQuery) error {
|
||||
metrics.M_DB_DataSource_QueryById.Inc(1)
|
||||
|
||||
datasource := m.DataSource{OrgId: query.OrgId, Id: query.Id}
|
||||
has, err := x.Get(&datasource)
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ func getEngine() (*xorm.Engine, error) {
|
||||
protocol = "unix"
|
||||
}
|
||||
|
||||
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8mb4",
|
||||
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&allowNativePasswords=true",
|
||||
DbCfg.User, DbCfg.Pwd, protocol, DbCfg.Host, DbCfg.Name)
|
||||
|
||||
if DbCfg.SslMode == "true" || DbCfg.SslMode == "skip-verify" {
|
||||
|
||||
@@ -12,7 +12,7 @@ type TestDB struct {
|
||||
}
|
||||
|
||||
var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:?_loc=Local"}
|
||||
var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?charset=utf8mb4"}
|
||||
var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci"}
|
||||
var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
|
||||
|
||||
func CleanDB(x *xorm.Engine) {
|
||||
|
||||
@@ -151,8 +151,12 @@ func (query *Query) renderMeasurement() string {
|
||||
func (query *Query) renderWhereClause() string {
|
||||
res := " WHERE "
|
||||
conditions := query.renderTags()
|
||||
res += strings.Join(conditions, " ")
|
||||
if len(conditions) > 0 {
|
||||
if len(conditions) > 1 {
|
||||
res += "(" + strings.Join(conditions, " ") + ")"
|
||||
} else {
|
||||
res += conditions[0]
|
||||
}
|
||||
res += " AND "
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
|
||||
|
||||
rawQuery, err := query.Build(queryContext)
|
||||
So(err, ShouldBeNil)
|
||||
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE "hostname" = 'server1' OR "hostname" = 'server2' AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`)
|
||||
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE ("hostname" = 'server1' OR "hostname" = 'server2') AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`)
|
||||
})
|
||||
|
||||
Convey("can build query with math part", func() {
|
||||
|
||||
@@ -71,7 +71,7 @@ func TestWildcardExpansion(t *testing.T) {
|
||||
So(expandeQueries[0].RawQuery, ShouldEqual, fmt.Sprintf("`os.cpu.3.idle`|aggregate.min|aggregate.max where cluster in ('demoapp-1', 'demoapp-2') and host in ('staples-lab-1', 'staples-lab-2') from %v to %v", from, to))
|
||||
})
|
||||
|
||||
Convey("Containg wildcard series", func() {
|
||||
Convey("Containing wildcard series", func() {
|
||||
query := &Query{
|
||||
Metrics: []Metric{
|
||||
{Metric: "os.cpu*", Alias: ""},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
@@ -65,7 +66,7 @@ func (e *MysqlExecutor) initEngine() error {
|
||||
}
|
||||
}
|
||||
|
||||
cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8mb4&parseTime=true&loc=UTC", e.datasource.User, e.datasource.Password, "tcp", e.datasource.Url, e.datasource.Database)
|
||||
cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC", e.datasource.User, e.datasource.Password, "tcp", e.datasource.Url, e.datasource.Database)
|
||||
e.log.Debug("getEngine", "connection", cnnstr)
|
||||
|
||||
engine, err := xorm.NewEngine("mysql", cnnstr)
|
||||
@@ -245,6 +246,7 @@ func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows)
|
||||
|
||||
func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
|
||||
pointsBySeries := make(map[string]*tsdb.TimeSeries)
|
||||
seriesByQueryOrder := list.New()
|
||||
columnNames, err := rows.Columns()
|
||||
|
||||
if err != nil {
|
||||
@@ -282,11 +284,13 @@ func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows,
|
||||
series := &tsdb.TimeSeries{Name: rowData.metric}
|
||||
series.Points = append(series.Points, tsdb.TimePoint{rowData.value, rowData.time})
|
||||
pointsBySeries[rowData.metric] = series
|
||||
seriesByQueryOrder.PushBack(rowData.metric)
|
||||
}
|
||||
}
|
||||
|
||||
for _, value := range pointsBySeries {
|
||||
result.Series = append(result.Series, value)
|
||||
for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() {
|
||||
key := elem.Value.(string)
|
||||
result.Series = append(result.Series, pointsBySeries[key])
|
||||
}
|
||||
|
||||
result.Meta.Set("rowCount", rowCount)
|
||||
|
||||
Reference in New Issue
Block a user