Merge branch 'master' into master
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@ import (
|
||||
func AdminGetSettings(c *m.ReqContext) {
|
||||
settings := make(map[string]interface{})
|
||||
|
||||
for _, section := range setting.Cfg.Sections() {
|
||||
for _, section := range setting.Raw.Sections() {
|
||||
jsonSec := make(map[string]interface{})
|
||||
settings[section.Name()] = jsonSec
|
||||
|
||||
|
||||
+9
-32
@@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@@ -15,9 +14,10 @@ import (
|
||||
func GetAnnotations(c *m.ReqContext) Response {
|
||||
|
||||
query := &annotations.ItemQuery{
|
||||
From: c.QueryInt64("from") / 1000,
|
||||
To: c.QueryInt64("to") / 1000,
|
||||
From: c.QueryInt64("from"),
|
||||
To: c.QueryInt64("to"),
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.QueryInt64("userId"),
|
||||
AlertId: c.QueryInt64("alertId"),
|
||||
DashboardId: c.QueryInt64("dashboardId"),
|
||||
PanelId: c.QueryInt64("panelId"),
|
||||
@@ -37,7 +37,7 @@ func GetAnnotations(c *m.ReqContext) Response {
|
||||
if item.Email != "" {
|
||||
item.AvatarUrl = dtos.GetGravatarUrl(item.Email)
|
||||
}
|
||||
item.Time = item.Time * 1000
|
||||
item.Time = item.Time
|
||||
}
|
||||
|
||||
return JSON(200, items)
|
||||
@@ -68,16 +68,12 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
|
||||
UserId: c.UserId,
|
||||
DashboardId: cmd.DashboardId,
|
||||
PanelId: cmd.PanelId,
|
||||
Epoch: cmd.Time / 1000,
|
||||
Epoch: cmd.Time,
|
||||
Text: cmd.Text,
|
||||
Data: cmd.Data,
|
||||
Tags: cmd.Tags,
|
||||
}
|
||||
|
||||
if item.Epoch == 0 {
|
||||
item.Epoch = time.Now().Unix()
|
||||
}
|
||||
|
||||
if err := repo.Save(&item); err != nil {
|
||||
return Error(500, "Failed to save annotation", err)
|
||||
}
|
||||
@@ -97,7 +93,7 @@ func PostAnnotation(c *m.ReqContext, cmd dtos.PostAnnotationsCmd) Response {
|
||||
}
|
||||
|
||||
item.Id = 0
|
||||
item.Epoch = cmd.TimeEnd / 1000
|
||||
item.Epoch = cmd.TimeEnd
|
||||
|
||||
if err := repo.Save(&item); err != nil {
|
||||
return Error(500, "Failed save annotation for region end time", err)
|
||||
@@ -132,9 +128,6 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
|
||||
return Error(500, "Failed to save Graphite annotation", err)
|
||||
}
|
||||
|
||||
if cmd.When == 0 {
|
||||
cmd.When = time.Now().Unix()
|
||||
}
|
||||
text := formatGraphiteAnnotation(cmd.What, cmd.Data)
|
||||
|
||||
// Support tags in prior to Graphite 0.10.0 format (string of tags separated by space)
|
||||
@@ -163,7 +156,7 @@ func PostGraphiteAnnotation(c *m.ReqContext, cmd dtos.PostGraphiteAnnotationsCmd
|
||||
item := annotations.Item{
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
Epoch: cmd.When,
|
||||
Epoch: cmd.When * 1000,
|
||||
Text: text,
|
||||
Tags: tagsArray,
|
||||
}
|
||||
@@ -191,7 +184,7 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
Id: annotationID,
|
||||
Epoch: cmd.Time / 1000,
|
||||
Epoch: cmd.Time,
|
||||
Text: cmd.Text,
|
||||
Tags: cmd.Tags,
|
||||
}
|
||||
@@ -203,7 +196,7 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response {
|
||||
if cmd.IsRegion {
|
||||
itemRight := item
|
||||
itemRight.RegionId = item.Id
|
||||
itemRight.Epoch = cmd.TimeEnd / 1000
|
||||
itemRight.Epoch = cmd.TimeEnd
|
||||
|
||||
// We don't know id of region right event, so set it to 0 and find then using query like
|
||||
// ... WHERE region_id = <item.RegionId> AND id != <item.RegionId> ...
|
||||
@@ -301,19 +294,3 @@ func canSave(c *m.ReqContext, repo annotations.Repository, annotationID int64) R
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func canSaveByRegionID(c *m.ReqContext, repo annotations.Repository, regionID int64) Response {
|
||||
items, err := repo.Find(&annotations.ItemQuery{RegionId: regionID, OrgId: c.OrgId})
|
||||
|
||||
if err != nil || len(items) == 0 {
|
||||
return Error(500, "Could not find annotation to update", err)
|
||||
}
|
||||
|
||||
dashboardID := items[0].DashboardId
|
||||
|
||||
if canSave, err := canSaveByDashboardID(c, dashboardID); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+12
-4
@@ -23,7 +23,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
// automatically set HEAD for every GET
|
||||
macaronR.SetAutoHead(true)
|
||||
|
||||
r := newRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)
|
||||
r := hs.RouteRegister
|
||||
|
||||
// not logged in views
|
||||
r.Get("/", reqSignedIn, Index)
|
||||
@@ -149,8 +149,6 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
|
||||
// team (admin permission required)
|
||||
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
|
||||
teamsRoute.Get("/:teamId", wrap(GetTeamByID))
|
||||
teamsRoute.Get("/search", wrap(SearchTeams))
|
||||
teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam))
|
||||
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
|
||||
teamsRoute.Delete("/:teamId", wrap(DeleteTeamByID))
|
||||
@@ -159,6 +157,12 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
// team without requirement of user to be org admin
|
||||
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
|
||||
teamsRoute.Get("/:teamId", wrap(GetTeamByID))
|
||||
teamsRoute.Get("/search", wrap(SearchTeams))
|
||||
})
|
||||
|
||||
// org information available to all users.
|
||||
apiRoute.Group("/org", func(orgRoute RouteRegister) {
|
||||
orgRoute.Get("/", wrap(GetOrgCurrent))
|
||||
@@ -170,7 +174,6 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrgCurrent))
|
||||
orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddressCurrent))
|
||||
orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
|
||||
orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
|
||||
orgRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
|
||||
orgRoute.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
|
||||
|
||||
@@ -184,6 +187,11 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
orgRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
// current org without requirement of user to be org admin
|
||||
apiRoute.Group("/org", func(orgRoute RouteRegister) {
|
||||
orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
|
||||
})
|
||||
|
||||
// create new org
|
||||
apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ func (this *thunderTask) Fetch() {
|
||||
this.Done()
|
||||
}
|
||||
|
||||
var client *http.Client = &http.Client{
|
||||
var client = &http.Client{
|
||||
Timeout: time.Second * 2,
|
||||
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
|
||||
}
|
||||
@@ -258,9 +258,6 @@ func (this *thunderTask) fetch() error {
|
||||
this.Avatar.data = &bytes.Buffer{}
|
||||
writer := bufio.NewWriter(this.Avatar.data)
|
||||
|
||||
if _, err = io.Copy(writer, resp.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err = io.Copy(writer, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
+12
-1
@@ -102,6 +102,16 @@ func GetDashboard(c *m.ReqContext) Response {
|
||||
meta.FolderUrl = query.Result.GetUrl()
|
||||
}
|
||||
|
||||
isDashboardProvisioned := &m.IsDashboardProvisionedQuery{DashboardId: dash.Id}
|
||||
err = bus.Dispatch(isDashboardProvisioned)
|
||||
if err != nil {
|
||||
return Error(500, "Error while checking if dashboard is provisioned", err)
|
||||
}
|
||||
|
||||
if isDashboardProvisioned.Result {
|
||||
meta.Provisioned = true
|
||||
}
|
||||
|
||||
// make sure db version is in sync with json model version
|
||||
dash.Data.Set("version", dash.Version)
|
||||
|
||||
@@ -228,7 +238,8 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
|
||||
err == m.ErrDashboardWithSameUIDExists ||
|
||||
err == m.ErrFolderNotFound ||
|
||||
err == m.ErrDashboardFolderCannotHaveParent ||
|
||||
err == m.ErrDashboardFolderNameExists {
|
||||
err == m.ErrDashboardFolderNameExists ||
|
||||
err == m.ErrDashboardCannotSaveProvisionedDashboard {
|
||||
return Error(400, err.Error(), nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,11 @@ func GetDashboardPermissionList(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
for _, perm := range acl {
|
||||
perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail)
|
||||
|
||||
if perm.TeamId > 0 {
|
||||
perm.TeamAvatarUrl = dtos.GetGravatarUrlWithDefault(perm.TeamEmail, perm.Team)
|
||||
}
|
||||
if perm.Slug != "" {
|
||||
perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When trying to override inherited permissions with lower presedence", func() {
|
||||
Convey("When trying to override inherited permissions with lower precedence", func() {
|
||||
origNewGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
|
||||
CanAdminValue: true,
|
||||
|
||||
@@ -42,6 +42,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
|
||||
query.Result = false
|
||||
return nil
|
||||
})
|
||||
|
||||
viewerRole := m.ROLE_VIEWER
|
||||
editorRole := m.ROLE_EDITOR
|
||||
|
||||
@@ -192,6 +197,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
fakeDash.HasAcl = true
|
||||
setting.ViewersCanEdit = false
|
||||
|
||||
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
|
||||
query.Result = false
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
|
||||
dashboards := []*m.Dashboard{fakeDash}
|
||||
query.Result = dashboards
|
||||
@@ -625,6 +635,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
dashTwo.FolderId = 3
|
||||
dashTwo.HasAcl = false
|
||||
|
||||
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
|
||||
query.Result = false
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardsBySlugQuery) error {
|
||||
dashboards := []*m.Dashboard{dashOne, dashTwo}
|
||||
query.Result = dashboards
|
||||
@@ -720,6 +735,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
{SaveError: m.ErrDashboardUpdateAccessDenied, ExpectedStatusCode: 403},
|
||||
{SaveError: m.ErrDashboardInvalidUid, ExpectedStatusCode: 400},
|
||||
{SaveError: m.ErrDashboardUidToLong, ExpectedStatusCode: 400},
|
||||
{SaveError: m.ErrDashboardCannotSaveProvisionedDashboard, ExpectedStatusCode: 400},
|
||||
{SaveError: m.UpdatePluginDashboardError{PluginId: "plug"}, ExpectedStatusCode: 412},
|
||||
}
|
||||
|
||||
@@ -750,6 +766,11 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error {
|
||||
query.Result = false
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
|
||||
query.Result = &m.DashboardVersion{
|
||||
Data: simplejson.NewFromAny(map[string]interface{}{
|
||||
|
||||
@@ -28,6 +28,7 @@ type DashboardMeta struct {
|
||||
FolderId int64 `json:"folderId"`
|
||||
FolderTitle string `json:"folderTitle"`
|
||||
FolderUrl string `json:"folderUrl"`
|
||||
Provisioned bool `json:"provisioned"`
|
||||
}
|
||||
|
||||
type DashboardFullWithMeta struct {
|
||||
|
||||
+16
-15
@@ -22,21 +22,22 @@ type LoginCommand struct {
|
||||
}
|
||||
|
||||
type CurrentUser struct {
|
||||
IsSignedIn bool `json:"isSignedIn"`
|
||||
Id int64 `json:"id"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
LightTheme bool `json:"lightTheme"`
|
||||
OrgCount int `json:"orgCount"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
OrgName string `json:"orgName"`
|
||||
OrgRole m.RoleType `json:"orgRole"`
|
||||
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
|
||||
GravatarUrl string `json:"gravatarUrl"`
|
||||
Timezone string `json:"timezone"`
|
||||
Locale string `json:"locale"`
|
||||
HelpFlags1 m.HelpFlags1 `json:"helpFlags1"`
|
||||
IsSignedIn bool `json:"isSignedIn"`
|
||||
Id int64 `json:"id"`
|
||||
Login string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
LightTheme bool `json:"lightTheme"`
|
||||
OrgCount int `json:"orgCount"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
OrgName string `json:"orgName"`
|
||||
OrgRole m.RoleType `json:"orgRole"`
|
||||
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
|
||||
GravatarUrl string `json:"gravatarUrl"`
|
||||
Timezone string `json:"timezone"`
|
||||
Locale string `json:"locale"`
|
||||
HelpFlags1 m.HelpFlags1 `json:"helpFlags1"`
|
||||
HasEditPermissionInFolders bool `json:"hasEditPermissionInFolders"`
|
||||
}
|
||||
|
||||
type MetricRequest struct {
|
||||
|
||||
@@ -33,6 +33,12 @@ func GetFolderPermissionList(c *m.ReqContext) Response {
|
||||
perm.FolderId = folder.Id
|
||||
perm.DashboardId = 0
|
||||
|
||||
perm.UserAvatarUrl = dtos.GetGravatarUrl(perm.UserEmail)
|
||||
|
||||
if perm.TeamId > 0 {
|
||||
perm.TeamAvatarUrl = dtos.GetGravatarUrlWithDefault(perm.TeamEmail, perm.Team)
|
||||
}
|
||||
|
||||
if perm.Slug != "" {
|
||||
perm.Url = m.GetDashboardFolderUrl(perm.IsFolder, perm.Uid, perm.Slug)
|
||||
}
|
||||
|
||||
+34
-15
@@ -26,27 +26,34 @@ import (
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.RegisterService(&HTTPServer{})
|
||||
}
|
||||
|
||||
type HTTPServer struct {
|
||||
log log.Logger
|
||||
macaron *macaron.Macaron
|
||||
context context.Context
|
||||
streamManager *live.StreamManager
|
||||
cache *gocache.Cache
|
||||
httpSrv *http.Server
|
||||
|
||||
httpSrv *http.Server
|
||||
RouteRegister RouteRegister `inject:""`
|
||||
Bus bus.Bus `inject:""`
|
||||
}
|
||||
|
||||
func NewHTTPServer() *HTTPServer {
|
||||
return &HTTPServer{
|
||||
log: log.New("http.server"),
|
||||
cache: gocache.New(5*time.Minute, 10*time.Minute),
|
||||
}
|
||||
func (hs *HTTPServer) Init() error {
|
||||
hs.log = log.New("http.server")
|
||||
hs.cache = gocache.New(5*time.Minute, 10*time.Minute)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Start(ctx context.Context) error {
|
||||
func (hs *HTTPServer) Run(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
hs.context = ctx
|
||||
@@ -57,9 +64,20 @@ func (hs *HTTPServer) Start(ctx context.Context) error {
|
||||
hs.streamManager.Run(ctx)
|
||||
|
||||
listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
|
||||
hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath)
|
||||
hs.log.Info("HTTP Server Listen", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath)
|
||||
|
||||
hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron}
|
||||
|
||||
// handle http shutdown on server context done
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
// Hacky fix for race condition between ListenAndServe and Shutdown
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
if err := hs.httpSrv.Shutdown(context.Background()); err != nil {
|
||||
hs.log.Error("Failed to shutdown server", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
switch setting.Protocol {
|
||||
case setting.HTTP:
|
||||
err = hs.httpSrv.ListenAndServe()
|
||||
@@ -96,12 +114,6 @@ func (hs *HTTPServer) Start(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Shutdown(ctx context.Context) error {
|
||||
err := hs.httpSrv.Shutdown(ctx)
|
||||
hs.log.Info("Stopped HTTP server")
|
||||
return err
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
|
||||
if certfile == "" {
|
||||
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
|
||||
@@ -139,7 +151,7 @@ func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
|
||||
}
|
||||
|
||||
hs.httpSrv.TLSConfig = tlsCfg
|
||||
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0)
|
||||
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
|
||||
|
||||
return hs.httpSrv.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
|
||||
}
|
||||
@@ -162,6 +174,7 @@ func (hs *HTTPServer) newMacaron() *macaron.Macaron {
|
||||
hs.mapStatic(m, route.Directory, "", pluginRoute)
|
||||
}
|
||||
|
||||
hs.mapStatic(m, setting.StaticRootPath, "build", "public/build")
|
||||
hs.mapStatic(m, setting.StaticRootPath, "", "public")
|
||||
hs.mapStatic(m, setting.StaticRootPath, "robots.txt", "robots.txt")
|
||||
|
||||
@@ -229,6 +242,12 @@ func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string,
|
||||
c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
|
||||
}
|
||||
|
||||
if prefix == "public/build" {
|
||||
headers = func(c *macaron.Context) {
|
||||
c.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
}
|
||||
}
|
||||
|
||||
if setting.Env == setting.DEV {
|
||||
headers = func(c *macaron.Context) {
|
||||
c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache")
|
||||
|
||||
+41
-16
@@ -42,23 +42,29 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
settings["appSubUrl"] = ""
|
||||
}
|
||||
|
||||
hasEditPermissionInFoldersQuery := m.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
|
||||
if err := bus.Dispatch(&hasEditPermissionInFoldersQuery); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data = dtos.IndexViewData{
|
||||
User: &dtos.CurrentUser{
|
||||
Id: c.UserId,
|
||||
IsSignedIn: c.IsSignedIn,
|
||||
Login: c.Login,
|
||||
Email: c.Email,
|
||||
Name: c.Name,
|
||||
OrgCount: c.OrgCount,
|
||||
OrgId: c.OrgId,
|
||||
OrgName: c.OrgName,
|
||||
OrgRole: c.OrgRole,
|
||||
GravatarUrl: dtos.GetGravatarUrl(c.Email),
|
||||
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
||||
LightTheme: prefs.Theme == "light",
|
||||
Timezone: prefs.Timezone,
|
||||
Locale: locale,
|
||||
HelpFlags1: c.HelpFlags1,
|
||||
Id: c.UserId,
|
||||
IsSignedIn: c.IsSignedIn,
|
||||
Login: c.Login,
|
||||
Email: c.Email,
|
||||
Name: c.Name,
|
||||
OrgCount: c.OrgCount,
|
||||
OrgId: c.OrgId,
|
||||
OrgName: c.OrgName,
|
||||
OrgRole: c.OrgRole,
|
||||
GravatarUrl: dtos.GetGravatarUrl(c.Email),
|
||||
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
||||
LightTheme: prefs.Theme == "light",
|
||||
Timezone: prefs.Timezone,
|
||||
Locale: locale,
|
||||
HelpFlags1: c.HelpFlags1,
|
||||
HasEditPermissionInFolders: hasEditPermissionInFoldersQuery.Result,
|
||||
},
|
||||
Settings: settings,
|
||||
Theme: prefs.Theme,
|
||||
@@ -117,10 +123,28 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
Children: dashboardChildNavs,
|
||||
})
|
||||
|
||||
if setting.ExploreEnabled {
|
||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||
Text: "Explore",
|
||||
Id: "explore",
|
||||
SubTitle: "Explore your data",
|
||||
Icon: "fa fa-rocket",
|
||||
Url: setting.AppSubUrl + "/explore",
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "New tab", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/explore"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if c.IsSignedIn {
|
||||
// Only set login if it's different from the name
|
||||
var login string
|
||||
if c.SignedInUser.Login != c.SignedInUser.NameOrFallback() {
|
||||
login = c.SignedInUser.Login
|
||||
}
|
||||
profileNode := &dtos.NavLink{
|
||||
Text: c.SignedInUser.NameOrFallback(),
|
||||
SubTitle: c.SignedInUser.Login,
|
||||
SubTitle: login,
|
||||
Id: "profile",
|
||||
Img: data.User.GravatarUrl,
|
||||
Url: setting.AppSubUrl + "/profile",
|
||||
@@ -284,6 +308,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
|
||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||
Text: "Help",
|
||||
SubTitle: fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit),
|
||||
Id: "help",
|
||||
Url: "#",
|
||||
Icon: "gicon gicon-question",
|
||||
|
||||
+6
-5
@@ -101,13 +101,14 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
|
||||
return Error(401, "Login is disabled", nil)
|
||||
}
|
||||
|
||||
authQuery := login.LoginUserQuery{
|
||||
Username: cmd.User,
|
||||
Password: cmd.Password,
|
||||
IpAddress: c.Req.RemoteAddr,
|
||||
authQuery := &m.LoginUserQuery{
|
||||
ReqContext: c,
|
||||
Username: cmd.User,
|
||||
Password: cmd.Password,
|
||||
IpAddress: c.Req.RemoteAddr,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&authQuery); err != nil {
|
||||
if err := bus.Dispatch(authQuery); err != nil {
|
||||
if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts {
|
||||
return Error(401, "Invalid username or password", err)
|
||||
}
|
||||
|
||||
+27
-46
@@ -6,7 +6,6 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@@ -16,22 +15,15 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/social"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
|
||||
ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
|
||||
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
|
||||
ErrUsersQuotaReached = errors.New("Users quota reached")
|
||||
ErrNoEmail = errors.New("Login provider didn't return an email address")
|
||||
oauthLogger = log.New("oauth")
|
||||
)
|
||||
var oauthLogger = log.New("oauth")
|
||||
|
||||
func GenStateString() string {
|
||||
rnd := make([]byte, 32)
|
||||
@@ -56,7 +48,7 @@ func OAuthLogin(ctx *m.ReqContext) {
|
||||
if errorParam != "" {
|
||||
errorDesc := ctx.Query("error_description")
|
||||
oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
|
||||
redirectWithError(ctx, ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
|
||||
redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -149,54 +141,43 @@ func OAuthLogin(ctx *m.ReqContext) {
|
||||
|
||||
// validate that we got at least an email address
|
||||
if userInfo.Email == "" {
|
||||
redirectWithError(ctx, ErrNoEmail)
|
||||
redirectWithError(ctx, login.ErrNoEmail)
|
||||
return
|
||||
}
|
||||
|
||||
// validate that the email is allowed to login to grafana
|
||||
if !connect.IsEmailAllowed(userInfo.Email) {
|
||||
redirectWithError(ctx, ErrEmailNotAllowed)
|
||||
redirectWithError(ctx, login.ErrEmailNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
userQuery := m.GetUserByEmailQuery{Email: userInfo.Email}
|
||||
err = bus.Dispatch(&userQuery)
|
||||
extUser := &m.ExternalUserInfo{
|
||||
AuthModule: "oauth_" + name,
|
||||
AuthId: userInfo.Id,
|
||||
Name: userInfo.Name,
|
||||
Login: userInfo.Login,
|
||||
Email: userInfo.Email,
|
||||
OrgRoles: map[int64]m.RoleType{},
|
||||
}
|
||||
|
||||
// create account if missing
|
||||
if err == m.ErrUserNotFound {
|
||||
if !connect.IsSignupAllowed() {
|
||||
redirectWithError(ctx, ErrSignUpNotAllowed)
|
||||
return
|
||||
}
|
||||
limitReached, err := quota.QuotaReached(ctx, "user")
|
||||
if err != nil {
|
||||
ctx.Handle(500, "Failed to get user quota", err)
|
||||
return
|
||||
}
|
||||
if limitReached {
|
||||
redirectWithError(ctx, ErrUsersQuotaReached)
|
||||
return
|
||||
}
|
||||
cmd := m.CreateUserCommand{
|
||||
Login: userInfo.Login,
|
||||
Email: userInfo.Email,
|
||||
Name: userInfo.Name,
|
||||
Company: userInfo.Company,
|
||||
DefaultOrgRole: userInfo.Role,
|
||||
}
|
||||
if userInfo.Role != "" {
|
||||
extUser.OrgRoles[1] = m.RoleType(userInfo.Role)
|
||||
}
|
||||
|
||||
if err = bus.Dispatch(&cmd); err != nil {
|
||||
ctx.Handle(500, "Failed to create account", err)
|
||||
return
|
||||
}
|
||||
|
||||
userQuery.Result = &cmd.Result
|
||||
} else if err != nil {
|
||||
ctx.Handle(500, "Unexpected error", err)
|
||||
// add/update user in grafana
|
||||
cmd := &m.UpsertUserCommand{
|
||||
ReqContext: ctx,
|
||||
ExternalUser: extUser,
|
||||
SignupAllowed: connect.IsSignupAllowed(),
|
||||
}
|
||||
err = bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
redirectWithError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
// login
|
||||
loginUserWithUser(userQuery.Result, ctx)
|
||||
loginUserWithUser(cmd.Result, ctx)
|
||||
|
||||
metrics.M_Api_Login_OAuth.Inc()
|
||||
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ func GetTestDataScenarios(c *m.ReqContext) Response {
|
||||
return JSON(200, &result)
|
||||
}
|
||||
|
||||
// Genereates a index out of range error
|
||||
// Generates a index out of range error
|
||||
func GenerateError(c *m.ReqContext) Response {
|
||||
var array []string
|
||||
return JSON(200, array[20])
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ func ValidateOrgPlaylist(c *m.ReqContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
if len(items) == 0 && c.Context.Req.Method != "DELETE" {
|
||||
c.JsonApiErr(404, "Playlist is empty", itemsErr)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -189,12 +189,6 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ type Router interface {
|
||||
Get(pattern string, handlers ...macaron.Handler) *macaron.Route
|
||||
}
|
||||
|
||||
// RouteRegister allows you to add routes and macaron.Handlers
|
||||
// that the web server should serve.
|
||||
type RouteRegister interface {
|
||||
Get(string, ...macaron.Handler)
|
||||
Post(string, ...macaron.Handler)
|
||||
@@ -26,7 +28,8 @@ type RouteRegister interface {
|
||||
|
||||
type RegisterNamedMiddleware func(name string) macaron.Handler
|
||||
|
||||
func newRouteRegister(namedMiddleware ...RegisterNamedMiddleware) RouteRegister {
|
||||
// NewRouteRegister creates a new RouteRegister with all middlewares sent as params
|
||||
func NewRouteRegister(namedMiddleware ...RegisterNamedMiddleware) RouteRegister {
|
||||
return &routeRegister{
|
||||
prefix: "",
|
||||
routes: []route{},
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestRouteSimpleRegister(t *testing.T) {
|
||||
}
|
||||
|
||||
// Setup
|
||||
rr := newRouteRegister(func(name string) macaron.Handler {
|
||||
rr := NewRouteRegister(func(name string) macaron.Handler {
|
||||
return emptyHandler(name)
|
||||
})
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestRouteGroupedRegister(t *testing.T) {
|
||||
}
|
||||
|
||||
// Setup
|
||||
rr := newRouteRegister()
|
||||
rr := NewRouteRegister()
|
||||
|
||||
rr.Delete("/admin", emptyHandler("1"))
|
||||
rr.Get("/down", emptyHandler("1"), emptyHandler("2"))
|
||||
@@ -150,7 +150,7 @@ func TestNamedMiddlewareRouteRegister(t *testing.T) {
|
||||
}
|
||||
|
||||
// Setup
|
||||
rr := newRouteRegister(func(name string) macaron.Handler {
|
||||
rr := NewRouteRegister(func(name string) macaron.Handler {
|
||||
return emptyHandler(name)
|
||||
})
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ type StaticOptions struct {
|
||||
// Expires defines which user-defined function to use for producing a HTTP Expires Header
|
||||
// https://developers.google.com/speed/docs/insights/LeverageBrowserCaching
|
||||
AddHeaders func(ctx *macaron.Context)
|
||||
// FileSystem is the interface for supporting any implmentation of file system.
|
||||
// FileSystem is the interface for supporting any implementation of file system.
|
||||
FileSystem http.FileSystem
|
||||
}
|
||||
|
||||
|
||||
+12
-7
@@ -2,7 +2,7 @@ package bus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
@@ -10,6 +10,8 @@ type HandlerFunc interface{}
|
||||
type CtxHandlerFunc func()
|
||||
type Msg interface{}
|
||||
|
||||
var ErrHandlerNotFound = errors.New("handler not found")
|
||||
|
||||
type Bus interface {
|
||||
Dispatch(msg Msg) error
|
||||
DispatchCtx(ctx context.Context, msg Msg) error
|
||||
@@ -38,12 +40,17 @@ func New() Bus {
|
||||
return bus
|
||||
}
|
||||
|
||||
// Want to get rid of global bus
|
||||
func GetBus() Bus {
|
||||
return globalBus
|
||||
}
|
||||
|
||||
func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error {
|
||||
var msgName = reflect.TypeOf(msg).Elem().Name()
|
||||
|
||||
var handler = b.handlers[msgName]
|
||||
if handler == nil {
|
||||
return fmt.Errorf("handler not found for %s", msgName)
|
||||
return ErrHandlerNotFound
|
||||
}
|
||||
|
||||
var params = make([]reflect.Value, 2)
|
||||
@@ -54,9 +61,8 @@ func (b *InProcBus) DispatchCtx(ctx context.Context, msg Msg) error {
|
||||
err := ret[0].Interface()
|
||||
if err == nil {
|
||||
return nil
|
||||
} else {
|
||||
return err.(error)
|
||||
}
|
||||
return err.(error)
|
||||
}
|
||||
|
||||
func (b *InProcBus) Dispatch(msg Msg) error {
|
||||
@@ -64,7 +70,7 @@ func (b *InProcBus) Dispatch(msg Msg) error {
|
||||
|
||||
var handler = b.handlers[msgName]
|
||||
if handler == nil {
|
||||
return fmt.Errorf("handler not found for %s", msgName)
|
||||
return ErrHandlerNotFound
|
||||
}
|
||||
|
||||
var params = make([]reflect.Value, 1)
|
||||
@@ -74,9 +80,8 @@ func (b *InProcBus) Dispatch(msg Msg) error {
|
||||
err := ret[0].Interface()
|
||||
if err == nil {
|
||||
return nil
|
||||
} else {
|
||||
return err.(error)
|
||||
}
|
||||
return err.(error)
|
||||
}
|
||||
|
||||
func (b *InProcBus) Publish(msg Msg) error {
|
||||
|
||||
@@ -15,7 +15,8 @@ func runDbCommand(command func(commandLine CommandLine) error) func(context *cli
|
||||
return func(context *cli.Context) {
|
||||
cmd := &contextCommandLine{context}
|
||||
|
||||
setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
cfg := setting.NewCfg()
|
||||
cfg.Load(&setting.CommandLineArgs{
|
||||
Config: cmd.String("config"),
|
||||
HomePath: cmd.String("homepath"),
|
||||
Args: flag.Args(),
|
||||
|
||||
@@ -33,7 +33,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
|
||||
fileInfo, err := os.Stat(pluginsDir)
|
||||
if err != nil {
|
||||
if err = os.MkdirAll(pluginsDir, os.ModePerm); err != nil {
|
||||
return errors.New(fmt.Sprintf("pluginsDir (%s) is not a directory", pluginsDir))
|
||||
return fmt.Errorf("pluginsDir (%s) is not a writable directory", pluginsDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ var validateLsCommand = func(pluginDir string) error {
|
||||
return fmt.Errorf("error: %s", err)
|
||||
}
|
||||
|
||||
if pluginDirInfo.IsDir() == false {
|
||||
if !pluginDirInfo.IsDir() {
|
||||
return errors.New("plugin path is not a directory")
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@ package commands
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
|
||||
services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||
"strings"
|
||||
|
||||
services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
|
||||
)
|
||||
|
||||
var getPluginss func(path string) []m.InstalledPlugin = services.GetLocalPlugins
|
||||
var removePlugin func(pluginPath, id string) error = services.RemoveInstalledPlugin
|
||||
|
||||
func removeCommand(c CommandLine) error {
|
||||
|
||||
@@ -53,8 +53,7 @@ func upgradeAllCommand(c CommandLine) error {
|
||||
for _, p := range pluginsToUpgrade {
|
||||
logger.Infof("Updating %v \n", p.Id)
|
||||
|
||||
var err error
|
||||
err = s.RemoveInstalledPlugin(pluginsDir, p.Id)
|
||||
err := s.RemoveInstalledPlugin(pluginsDir, p.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
||||
@@ -42,7 +43,7 @@ func Init(version string, skipTLSVerify bool) {
|
||||
}
|
||||
|
||||
HttpClient = http.Client{
|
||||
Timeout: time.Duration(10 * time.Second),
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: tr,
|
||||
}
|
||||
}
|
||||
@@ -155,6 +156,8 @@ func sendRequest(repoUrl string, subPaths ...string) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
|
||||
req.Header.Set("grafana-version", grafanaVersion)
|
||||
req.Header.Set("grafana-os", runtime.GOOS)
|
||||
req.Header.Set("grafana-arch", runtime.GOARCH)
|
||||
req.Header.Set("User-Agent", "grafana "+grafanaVersion)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
_ "github.com/grafana/grafana/pkg/extensions"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/cloudwatch"
|
||||
@@ -33,12 +34,11 @@ import (
|
||||
var version = "5.0.0"
|
||||
var commit = "NA"
|
||||
var buildstamp string
|
||||
var build_date string
|
||||
var enterprise string
|
||||
|
||||
var configFile = flag.String("config", "", "path to config file")
|
||||
var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
|
||||
var pidFile = flag.String("pidfile", "", "path to pid file")
|
||||
var exitChan = make(chan int)
|
||||
|
||||
func main() {
|
||||
v := flag.Bool("v", false, "prints current version and exits")
|
||||
@@ -77,45 +77,31 @@ func main() {
|
||||
setting.BuildVersion = version
|
||||
setting.BuildCommit = commit
|
||||
setting.BuildStamp = buildstampInt64
|
||||
setting.Enterprise, _ = strconv.ParseBool(enterprise)
|
||||
|
||||
metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
|
||||
shutdownCompleted := make(chan int)
|
||||
|
||||
server := NewGrafanaServer()
|
||||
|
||||
go listenToSystemSignals(server, shutdownCompleted)
|
||||
go listenToSystemSignals(server)
|
||||
|
||||
go func() {
|
||||
code := 0
|
||||
if err := server.Start(); err != nil {
|
||||
log.Error2("Startup failed", "error", err)
|
||||
code = 1
|
||||
}
|
||||
err := server.Run()
|
||||
|
||||
exitChan <- code
|
||||
}()
|
||||
|
||||
code := <-shutdownCompleted
|
||||
log.Info2("Grafana shutdown completed.", "code", code)
|
||||
trace.Stop()
|
||||
log.Close()
|
||||
os.Exit(code)
|
||||
|
||||
server.Exit(err)
|
||||
}
|
||||
|
||||
func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int) {
|
||||
func listenToSystemSignals(server *GrafanaServerImpl) {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
ignoreChan := make(chan os.Signal, 1)
|
||||
code := 0
|
||||
|
||||
signal.Notify(ignoreChan, syscall.SIGHUP)
|
||||
signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case sig := <-signalChan:
|
||||
trace.Stop() // Stops trace if profiling has been enabled
|
||||
server.Shutdown(0, fmt.Sprintf("system signal: %s", sig))
|
||||
shutdownCompleted <- 0
|
||||
case code = <-exitChan:
|
||||
trace.Stop() // Stops trace if profiling has been enabled
|
||||
server.Shutdown(code, "startup error")
|
||||
shutdownCompleted <- code
|
||||
server.Shutdown(fmt.Sprintf("System signal: %s", sig))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,27 +8,36 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/facebookgo/inject"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/cleanup"
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/grafana/grafana/pkg/social"
|
||||
"github.com/grafana/grafana/pkg/tracing"
|
||||
|
||||
// self registering services
|
||||
_ "github.com/grafana/grafana/pkg/extensions"
|
||||
_ "github.com/grafana/grafana/pkg/metrics"
|
||||
_ "github.com/grafana/grafana/pkg/plugins"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting"
|
||||
_ "github.com/grafana/grafana/pkg/services/cleanup"
|
||||
_ "github.com/grafana/grafana/pkg/services/notifications"
|
||||
_ "github.com/grafana/grafana/pkg/services/provisioning"
|
||||
_ "github.com/grafana/grafana/pkg/services/search"
|
||||
)
|
||||
|
||||
func NewGrafanaServer() *GrafanaServerImpl {
|
||||
@@ -40,110 +49,152 @@ func NewGrafanaServer() *GrafanaServerImpl {
|
||||
shutdownFn: shutdownFn,
|
||||
childRoutines: childRoutines,
|
||||
log: log.New("server"),
|
||||
cfg: setting.NewCfg(),
|
||||
}
|
||||
}
|
||||
|
||||
type GrafanaServerImpl struct {
|
||||
context context.Context
|
||||
shutdownFn context.CancelFunc
|
||||
childRoutines *errgroup.Group
|
||||
log log.Logger
|
||||
context context.Context
|
||||
shutdownFn context.CancelFunc
|
||||
childRoutines *errgroup.Group
|
||||
log log.Logger
|
||||
cfg *setting.Cfg
|
||||
shutdownReason string
|
||||
shutdownInProgress bool
|
||||
|
||||
httpServer *api.HTTPServer
|
||||
RouteRegister api.RouteRegister `inject:""`
|
||||
HttpServer *api.HTTPServer `inject:""`
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Start() error {
|
||||
g.initLogging()
|
||||
func (g *GrafanaServerImpl) Run() error {
|
||||
g.loadConfiguration()
|
||||
g.writePIDFile()
|
||||
|
||||
initSql()
|
||||
// initSql
|
||||
sqlstore.NewEngine() // TODO: this should return an error
|
||||
sqlstore.EnsureAdminUser()
|
||||
|
||||
metrics.Init(setting.Cfg)
|
||||
search.Init()
|
||||
login.Init()
|
||||
social.NewOAuthService()
|
||||
|
||||
pluginManager, err := plugins.NewPluginManager(g.context)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to start plugins. error: %v", err)
|
||||
}
|
||||
g.childRoutines.Go(func() error { return pluginManager.Run(g.context) })
|
||||
|
||||
if err := provisioning.Init(g.context, setting.HomePath, setting.Cfg); err != nil {
|
||||
return fmt.Errorf("Failed to provision Grafana from config. error: %v", err)
|
||||
}
|
||||
|
||||
tracingCloser, err := tracing.Init(setting.Cfg)
|
||||
tracingCloser, err := tracing.Init(g.cfg.Raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Tracing settings is not valid. error: %v", err)
|
||||
}
|
||||
defer tracingCloser.Close()
|
||||
|
||||
// init alerting
|
||||
if setting.AlertingEnabled && setting.ExecuteAlerts {
|
||||
engine := alerting.NewEngine()
|
||||
g.childRoutines.Go(func() error { return engine.Run(g.context) })
|
||||
serviceGraph := inject.Graph{}
|
||||
serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
|
||||
serviceGraph.Provide(&inject.Object{Value: g.cfg})
|
||||
serviceGraph.Provide(&inject.Object{Value: dashboards.NewProvisioningService()})
|
||||
serviceGraph.Provide(&inject.Object{Value: api.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
|
||||
|
||||
// self registered services
|
||||
services := registry.GetServices()
|
||||
|
||||
// Add all services to dependency graph
|
||||
for _, service := range services {
|
||||
serviceGraph.Provide(&inject.Object{Value: service})
|
||||
}
|
||||
|
||||
// cleanup service
|
||||
cleanUpService := cleanup.NewCleanUpService()
|
||||
g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) })
|
||||
serviceGraph.Provide(&inject.Object{Value: g})
|
||||
|
||||
if err = notifications.Init(); err != nil {
|
||||
return fmt.Errorf("Notification service failed to initialize. error: %v", err)
|
||||
// Inject dependencies to services
|
||||
if err := serviceGraph.Populate(); err != nil {
|
||||
return fmt.Errorf("Failed to populate service dependency: %v", err)
|
||||
}
|
||||
|
||||
// Init & start services
|
||||
for _, service := range services {
|
||||
if registry.IsDisabled(service) {
|
||||
continue
|
||||
}
|
||||
|
||||
g.log.Info("Initializing " + reflect.TypeOf(service).Elem().Name())
|
||||
|
||||
if err := service.Init(); err != nil {
|
||||
return fmt.Errorf("Service init failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start background services
|
||||
for index := range services {
|
||||
service, ok := services[index].(registry.BackgroundService)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if registry.IsDisabled(services[index]) {
|
||||
continue
|
||||
}
|
||||
|
||||
g.childRoutines.Go(func() error {
|
||||
// Skip starting new service when shutting down
|
||||
// Can happen when service stop/return during startup
|
||||
if g.shutdownInProgress {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := service.Run(g.context)
|
||||
|
||||
// If error is not canceled then the service crashed
|
||||
if err != context.Canceled && err != nil {
|
||||
g.log.Error("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err)
|
||||
} else {
|
||||
g.log.Info("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err)
|
||||
}
|
||||
|
||||
// Mark that we are in shutdown mode
|
||||
// So more services are not started
|
||||
g.shutdownInProgress = true
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
sendSystemdNotification("READY=1")
|
||||
|
||||
return g.startHttpServer()
|
||||
return g.childRoutines.Wait()
|
||||
}
|
||||
|
||||
func initSql() {
|
||||
sqlstore.NewEngine()
|
||||
sqlstore.EnsureAdminUser()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) initLogging() {
|
||||
err := setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
func (g *GrafanaServerImpl) loadConfiguration() {
|
||||
err := g.cfg.Load(&setting.CommandLineArgs{
|
||||
Config: *configFile,
|
||||
HomePath: *homePath,
|
||||
Args: flag.Args(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
g.log.Error(err.Error())
|
||||
fmt.Fprintf(os.Stderr, "Failed to start grafana. error: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
g.log.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
|
||||
setting.LogConfigurationInfo()
|
||||
g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
|
||||
g.cfg.LogConfigSources()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) startHttpServer() error {
|
||||
g.httpServer = api.NewHTTPServer()
|
||||
|
||||
err := g.httpServer.Start(g.context)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Fail to start server. error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
|
||||
g.log.Info("Shutdown started", "code", code, "reason", reason)
|
||||
|
||||
err := g.httpServer.Shutdown(g.context)
|
||||
if err != nil {
|
||||
g.log.Error("Failed to shutdown server", "error", err)
|
||||
}
|
||||
func (g *GrafanaServerImpl) Shutdown(reason string) {
|
||||
g.log.Info("Shutdown started", "reason", reason)
|
||||
g.shutdownReason = reason
|
||||
g.shutdownInProgress = true
|
||||
|
||||
// call cancel func on root context
|
||||
g.shutdownFn()
|
||||
err = g.childRoutines.Wait()
|
||||
if err != nil && err != context.Canceled {
|
||||
g.log.Error("Server shutdown completed with an error", "error", err)
|
||||
|
||||
// wait for child routines
|
||||
g.childRoutines.Wait()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) Exit(reason error) {
|
||||
// default exit code is 1
|
||||
code := 1
|
||||
|
||||
if reason == context.Canceled && g.shutdownReason != "" {
|
||||
reason = fmt.Errorf(g.shutdownReason)
|
||||
code = 0
|
||||
}
|
||||
|
||||
g.log.Error("Server shutdown", "reason", reason)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) writePIDFile() {
|
||||
|
||||
@@ -33,7 +33,7 @@ func New(orgId int64, name string) KeyGenResult {
|
||||
|
||||
jsonString, _ := json.Marshal(jsonKey)
|
||||
|
||||
result.ClientSecret = base64.StdEncoding.EncodeToString([]byte(jsonString))
|
||||
result.ClientSecret = base64.StdEncoding.EncodeToString(jsonString)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func Decode(keyString string) (*ApiKeyJson, error) {
|
||||
}
|
||||
|
||||
var keyObj ApiKeyJson
|
||||
err = json.Unmarshal([]byte(jsonString), &keyObj)
|
||||
err = json.Unmarshal(jsonString, &keyObj)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidApiKey
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
diff "github.com/yudai/gojsondiff"
|
||||
deltaFormatter "github.com/yudai/gojsondiff/formatter"
|
||||
@@ -15,11 +14,8 @@ import (
|
||||
var (
|
||||
// ErrUnsupportedDiffType occurs when an invalid diff type is used.
|
||||
ErrUnsupportedDiffType = errors.New("dashdiff: unsupported diff type")
|
||||
|
||||
// ErrNilDiff occurs when two compared interfaces are identical.
|
||||
ErrNilDiff = errors.New("dashdiff: diff is nil")
|
||||
|
||||
diffLogger = log.New("dashdiffs")
|
||||
)
|
||||
|
||||
type DiffType int
|
||||
@@ -145,5 +141,9 @@ func getDiff(baseData, newData *simplejson.Json) (interface{}, diff.Diff, error)
|
||||
|
||||
left := make(map[string]interface{})
|
||||
err = json.Unmarshal(leftBytes, &left)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return left, jsonDiff, nil
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// changeTypeToSymbol is used for populating the terminating characer in
|
||||
// changeTypeToSymbol is used for populating the terminating character in
|
||||
// the diff
|
||||
changeTypeToSymbol = map[ChangeType]string{
|
||||
ChangeNil: "",
|
||||
|
||||
+109
-184
@@ -134,9 +134,8 @@ func (v *Value) get(key string) (*Value, error) {
|
||||
child, ok := obj.Map()[key]
|
||||
if ok {
|
||||
return child, nil
|
||||
} else {
|
||||
return nil, KeyNotFoundError{key}
|
||||
}
|
||||
return nil, KeyNotFoundError{key}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
@@ -174,17 +173,13 @@ func (v *Object) GetObject(keys ...string) (*Object, error) {
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
|
||||
obj, err := child.Object()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
}
|
||||
obj, err := child.Object()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// Gets the value at key path and attempts to typecast the value into a string.
|
||||
@@ -196,18 +191,17 @@ func (v *Object) GetString(keys ...string) (string, error) {
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return child.String()
|
||||
}
|
||||
return child.String()
|
||||
}
|
||||
|
||||
func (v *Object) MustGetString(path string, def string) string {
|
||||
keys := strings.Split(path, ".")
|
||||
if str, err := v.GetString(keys...); err != nil {
|
||||
str, err := v.GetString(keys...)
|
||||
if err != nil {
|
||||
return def
|
||||
} else {
|
||||
return str
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// Gets the value at key path and attempts to typecast the value into null.
|
||||
@@ -233,16 +227,13 @@ func (v *Object) GetNumber(keys ...string) (json.Number, error) {
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
|
||||
n, err := child.Number()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
n, err := child.Number()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Gets the value at key path and attempts to typecast the value into a float64.
|
||||
@@ -254,16 +245,13 @@ func (v *Object) GetFloat64(keys ...string) (float64, error) {
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
|
||||
n, err := child.Float64()
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
n, err := child.Float64()
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Gets the value at key path and attempts to typecast the value into a float64.
|
||||
@@ -275,16 +263,13 @@ func (v *Object) GetInt64(keys ...string) (int64, error) {
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
|
||||
n, err := child.Int64()
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
n, err := child.Int64()
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Gets the value at key path and attempts to typecast the value into a float64.
|
||||
@@ -296,9 +281,8 @@ func (v *Object) GetInterface(keys ...string) (interface{}, error) {
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return child.Interface(), nil
|
||||
}
|
||||
return child.Interface(), nil
|
||||
}
|
||||
|
||||
// Gets the value at key path and attempts to typecast the value into a bool.
|
||||
@@ -311,7 +295,6 @@ func (v *Object) GetBoolean(keys ...string) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return child.Boolean()
|
||||
}
|
||||
|
||||
@@ -328,11 +311,8 @@ func (v *Object) GetValueArray(keys ...string) ([]*Value, error) {
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
|
||||
return child.Array()
|
||||
|
||||
}
|
||||
return child.Array()
|
||||
}
|
||||
|
||||
// Gets the value at key path and attempts to typecast the value into an array of objects.
|
||||
@@ -347,30 +327,24 @@ func (v *Object) GetObjectArray(keys ...string) ([]*Object, error) {
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
}
|
||||
array, err := child.Array()
|
||||
|
||||
array, err := child.Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
typedArray := make([]*Object, len(array))
|
||||
|
||||
for index, arrayItem := range array {
|
||||
typedArrayItem, err := arrayItem.
|
||||
Object()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
|
||||
typedArray := make([]*Object, len(array))
|
||||
|
||||
for index, arrayItem := range array {
|
||||
typedArrayItem, err := arrayItem.
|
||||
Object()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
typedArray[index] = typedArrayItem
|
||||
}
|
||||
|
||||
}
|
||||
return typedArray, nil
|
||||
}
|
||||
typedArray[index] = typedArrayItem
|
||||
}
|
||||
return typedArray, nil
|
||||
}
|
||||
|
||||
// Gets the value at key path and attempts to typecast the value into an array of string.
|
||||
@@ -387,29 +361,23 @@ func (v *Object) GetStringArray(keys ...string) ([]string, error) {
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
}
|
||||
array, err := child.Array()
|
||||
|
||||
array, err := child.Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
typedArray := make([]string, len(array))
|
||||
|
||||
for index, arrayItem := range array {
|
||||
typedArrayItem, err := arrayItem.String()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
|
||||
typedArray := make([]string, len(array))
|
||||
|
||||
for index, arrayItem := range array {
|
||||
typedArrayItem, err := arrayItem.String()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
typedArray[index] = typedArrayItem
|
||||
}
|
||||
|
||||
}
|
||||
return typedArray, nil
|
||||
}
|
||||
typedArray[index] = typedArrayItem
|
||||
}
|
||||
return typedArray, nil
|
||||
}
|
||||
|
||||
// Gets the value at key path and attempts to typecast the value into an array of numbers.
|
||||
@@ -424,29 +392,23 @@ func (v *Object) GetNumberArray(keys ...string) ([]json.Number, error) {
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
}
|
||||
array, err := child.Array()
|
||||
|
||||
array, err := child.Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
typedArray := make([]json.Number, len(array))
|
||||
|
||||
for index, arrayItem := range array {
|
||||
typedArrayItem, err := arrayItem.Number()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
|
||||
typedArray := make([]json.Number, len(array))
|
||||
|
||||
for index, arrayItem := range array {
|
||||
typedArrayItem, err := arrayItem.Number()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
typedArray[index] = typedArrayItem
|
||||
}
|
||||
|
||||
}
|
||||
return typedArray, nil
|
||||
}
|
||||
typedArray[index] = typedArrayItem
|
||||
}
|
||||
return typedArray, nil
|
||||
}
|
||||
|
||||
// Gets the value at key path and attempts to typecast the value into an array of floats.
|
||||
@@ -456,29 +418,23 @@ func (v *Object) GetFloat64Array(keys ...string) ([]float64, error) {
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
}
|
||||
array, err := child.Array()
|
||||
|
||||
array, err := child.Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
typedArray := make([]float64, len(array))
|
||||
|
||||
for index, arrayItem := range array {
|
||||
typedArrayItem, err := arrayItem.Float64()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
|
||||
typedArray := make([]float64, len(array))
|
||||
|
||||
for index, arrayItem := range array {
|
||||
typedArrayItem, err := arrayItem.Float64()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
typedArray[index] = typedArrayItem
|
||||
}
|
||||
|
||||
}
|
||||
return typedArray, nil
|
||||
}
|
||||
typedArray[index] = typedArrayItem
|
||||
}
|
||||
return typedArray, nil
|
||||
}
|
||||
|
||||
// Gets the value at key path and attempts to typecast the value into an array of ints.
|
||||
@@ -488,29 +444,23 @@ func (v *Object) GetInt64Array(keys ...string) ([]int64, error) {
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
}
|
||||
array, err := child.Array()
|
||||
|
||||
array, err := child.Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
typedArray := make([]int64, len(array))
|
||||
|
||||
for index, arrayItem := range array {
|
||||
typedArrayItem, err := arrayItem.Int64()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
|
||||
typedArray := make([]int64, len(array))
|
||||
|
||||
for index, arrayItem := range array {
|
||||
typedArrayItem, err := arrayItem.Int64()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
typedArray[index] = typedArrayItem
|
||||
}
|
||||
|
||||
}
|
||||
return typedArray, nil
|
||||
}
|
||||
typedArray[index] = typedArrayItem
|
||||
}
|
||||
return typedArray, nil
|
||||
}
|
||||
|
||||
// Gets the value at key path and attempts to typecast the value into an array of bools.
|
||||
@@ -520,29 +470,23 @@ func (v *Object) GetBooleanArray(keys ...string) ([]bool, error) {
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
}
|
||||
array, err := child.Array()
|
||||
|
||||
array, err := child.Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
typedArray := make([]bool, len(array))
|
||||
|
||||
for index, arrayItem := range array {
|
||||
typedArrayItem, err := arrayItem.Boolean()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
|
||||
typedArray := make([]bool, len(array))
|
||||
|
||||
for index, arrayItem := range array {
|
||||
typedArrayItem, err := arrayItem.Boolean()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
typedArray[index] = typedArrayItem
|
||||
}
|
||||
|
||||
}
|
||||
return typedArray, nil
|
||||
}
|
||||
typedArray[index] = typedArrayItem
|
||||
}
|
||||
return typedArray, nil
|
||||
}
|
||||
|
||||
// Gets the value at key path and attempts to typecast the value into an array of nulls.
|
||||
@@ -552,29 +496,23 @@ func (v *Object) GetNullArray(keys ...string) (int64, error) {
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
}
|
||||
array, err := child.Array()
|
||||
|
||||
array, err := child.Array()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var length int64 = 0
|
||||
|
||||
for _, arrayItem := range array {
|
||||
err := arrayItem.Null()
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
|
||||
var length int64 = 0
|
||||
|
||||
for _, arrayItem := range array {
|
||||
err := arrayItem.Null()
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
length++
|
||||
}
|
||||
|
||||
}
|
||||
return length, nil
|
||||
}
|
||||
length++
|
||||
}
|
||||
return length, nil
|
||||
}
|
||||
|
||||
// Returns an error if the value is not actually null
|
||||
@@ -585,15 +523,12 @@ func (v *Value) Null() error {
|
||||
switch v.data.(type) {
|
||||
case nil:
|
||||
valid = v.exists // Valid only if j also exists, since other values could possibly also be nil
|
||||
break
|
||||
}
|
||||
|
||||
if valid {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrNotNull
|
||||
|
||||
}
|
||||
|
||||
// Attempts to typecast the current value into an array.
|
||||
@@ -607,24 +542,19 @@ func (v *Value) Array() ([]*Value, error) {
|
||||
switch v.data.(type) {
|
||||
case []interface{}:
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
|
||||
// Unsure if this is a good way to use slices, it's probably not
|
||||
var slice []*Value
|
||||
|
||||
if valid {
|
||||
|
||||
for _, element := range v.data.([]interface{}) {
|
||||
child := Value{element, true}
|
||||
slice = append(slice, &child)
|
||||
}
|
||||
|
||||
return slice, nil
|
||||
}
|
||||
|
||||
return slice, ErrNotArray
|
||||
|
||||
}
|
||||
|
||||
// Attempts to typecast the current value into a number.
|
||||
@@ -638,7 +568,6 @@ func (v *Value) Number() (json.Number, error) {
|
||||
switch v.data.(type) {
|
||||
case json.Number:
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
|
||||
if valid {
|
||||
@@ -687,7 +616,6 @@ func (v *Value) Boolean() (bool, error) {
|
||||
switch v.data.(type) {
|
||||
case bool:
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
|
||||
if valid {
|
||||
@@ -709,7 +637,6 @@ func (v *Value) Object() (*Object, error) {
|
||||
switch v.data.(type) {
|
||||
case map[string]interface{}:
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
|
||||
if valid {
|
||||
@@ -746,7 +673,6 @@ func (v *Value) ObjectArray() ([]*Object, error) {
|
||||
switch v.data.(type) {
|
||||
case []interface{}:
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
|
||||
// Unsure if this is a good way to use slices, it's probably not
|
||||
@@ -782,7 +708,6 @@ func (v *Value) String() (string, error) {
|
||||
switch v.data.(type) {
|
||||
case string:
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
|
||||
if valid {
|
||||
|
||||
@@ -21,7 +21,7 @@ func NewAssert(t *testing.T) *Assert {
|
||||
}
|
||||
|
||||
func (assert *Assert) True(value bool, message string) {
|
||||
if value == false {
|
||||
if !value {
|
||||
log.Panicln("Assert: ", message)
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ func TestFirst(t *testing.T) {
|
||||
}`
|
||||
|
||||
j, err := NewObjectFromBytes([]byte(testJSON))
|
||||
assert.True(err == nil, "failed to create new object from bytes")
|
||||
|
||||
a, err := j.GetObject("address")
|
||||
assert.True(a != nil && err == nil, "failed to create json from string")
|
||||
@@ -76,10 +77,10 @@ func TestFirst(t *testing.T) {
|
||||
assert.True(s == "fallback", "must get string return fallback")
|
||||
|
||||
s, err = j.GetString("name")
|
||||
assert.True(s == "anton" && err == nil, "name shoud match")
|
||||
assert.True(s == "anton" && err == nil, "name should match")
|
||||
|
||||
s, err = j.GetString("address", "street")
|
||||
assert.True(s == "Street 42" && err == nil, "street shoud match")
|
||||
assert.True(s == "Street 42" && err == nil, "street should match")
|
||||
//log.Println("s: ", s.String())
|
||||
|
||||
_, err = j.GetNumber("age")
|
||||
@@ -108,6 +109,7 @@ func TestFirst(t *testing.T) {
|
||||
//log.Println("address: ", address)
|
||||
|
||||
s, err = address.GetString("street")
|
||||
assert.True(s == "Street 42" && err == nil, "street mismatching")
|
||||
|
||||
addressAsString, err := j.GetString("address")
|
||||
assert.True(addressAsString == "" && err != nil, "address should not be an string")
|
||||
@@ -119,13 +121,13 @@ func TestFirst(t *testing.T) {
|
||||
assert.True(s == "" && err != nil, "nonexistent string fail")
|
||||
|
||||
b, err := j.GetBoolean("true")
|
||||
assert.True(b == true && err == nil, "bool true test")
|
||||
assert.True(b && err == nil, "bool true test")
|
||||
|
||||
b, err = j.GetBoolean("false")
|
||||
assert.True(b == false && err == nil, "bool false test")
|
||||
assert.True(!b && err == nil, "bool false test")
|
||||
|
||||
b, err = j.GetBoolean("invalid_field")
|
||||
assert.True(b == false && err != nil, "bool invalid test")
|
||||
assert.True(!b && err != nil, "bool invalid test")
|
||||
|
||||
list, err := j.GetValueArray("list")
|
||||
assert.True(list != nil && err == nil, "list should be an array")
|
||||
@@ -148,6 +150,7 @@ func TestFirst(t *testing.T) {
|
||||
//assert.True(element.IsObject() == true, "first fail")
|
||||
|
||||
element, err := elementValue.Object()
|
||||
assert.True(err == nil, "create element fail")
|
||||
|
||||
s, err = element.GetString("street")
|
||||
assert.True(s == "Street 42" && err == nil, "second fail")
|
||||
@@ -232,6 +235,7 @@ func TestSecond(t *testing.T) {
|
||||
assert.True(fromName == "Tom Brady" && err == nil, "fromName mismatch")
|
||||
|
||||
actions, err := dataItem.GetObjectArray("actions")
|
||||
assert.True(err == nil, "get object from array failed")
|
||||
|
||||
for index, action := range actions {
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ func (a *Auth) SignRequest(req *http.Request) {
|
||||
)
|
||||
decodedKey, _ := base64.StdEncoding.DecodeString(a.Key)
|
||||
|
||||
sha256 := hmac.New(sha256.New, []byte(decodedKey))
|
||||
sha256 := hmac.New(sha256.New, decodedKey)
|
||||
sha256.Write([]byte(strToSign))
|
||||
|
||||
signature := base64.StdEncoding.EncodeToString(sha256.Sum(nil))
|
||||
|
||||
@@ -10,9 +10,11 @@ import (
|
||||
|
||||
func TestUploadToAzureBlob(t *testing.T) {
|
||||
SkipConvey("[Integration test] for external_image_store.azure_blob", t, func() {
|
||||
err := setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
cfg := setting.NewCfg()
|
||||
err := cfg.Load(&setting.CommandLineArgs{
|
||||
HomePath: "../../../",
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
uploader, _ := NewImageUploader()
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
|
||||
func TestUploadToGCS(t *testing.T) {
|
||||
SkipConvey("[Integration test] for external_image_store.gcs", t, func() {
|
||||
setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
cfg := setting.NewCfg()
|
||||
cfg.Load(&setting.CommandLineArgs{
|
||||
HomePath: "../../../",
|
||||
})
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@ package imguploader
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -24,7 +25,7 @@ func NewImageUploader() (ImageUploader, error) {
|
||||
|
||||
switch setting.ImageUploadProvider {
|
||||
case "s3":
|
||||
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
|
||||
s3sec, err := setting.Raw.GetSection("external_image_storage.s3")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -51,7 +52,7 @@ func NewImageUploader() (ImageUploader, error) {
|
||||
|
||||
return NewS3Uploader(region, bucket, path, "public-read", accessKey, secretKey), nil
|
||||
case "webdav":
|
||||
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
|
||||
webdavSec, err := setting.Raw.GetSection("external_image_storage.webdav")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -67,7 +68,7 @@ func NewImageUploader() (ImageUploader, error) {
|
||||
|
||||
return NewWebdavImageUploader(url, username, password, public_url)
|
||||
case "gcs":
|
||||
gcssec, err := setting.Cfg.GetSection("external_image_storage.gcs")
|
||||
gcssec, err := setting.Raw.GetSection("external_image_storage.gcs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -78,7 +79,7 @@ func NewImageUploader() (ImageUploader, error) {
|
||||
|
||||
return NewGCSUploader(keyFile, bucketName, path), nil
|
||||
case "azure_blob":
|
||||
azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob")
|
||||
azureBlobSec, err := setting.Raw.GetSection("external_image_storage.azure_blob")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -11,14 +11,16 @@ import (
|
||||
func TestImageUploaderFactory(t *testing.T) {
|
||||
Convey("Can create image uploader for ", t, func() {
|
||||
Convey("S3ImageUploader config", func() {
|
||||
setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
cfg := setting.NewCfg()
|
||||
cfg.Load(&setting.CommandLineArgs{
|
||||
HomePath: "../../../",
|
||||
})
|
||||
|
||||
setting.ImageUploadProvider = "s3"
|
||||
|
||||
Convey("with bucket url https://foo.bar.baz.s3-us-east-2.amazonaws.com", func() {
|
||||
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
|
||||
s3sec, err := setting.Raw.GetSection("external_image_storage.s3")
|
||||
So(err, ShouldBeNil)
|
||||
s3sec.NewKey("bucket_url", "https://foo.bar.baz.s3-us-east-2.amazonaws.com")
|
||||
s3sec.NewKey("access_key", "access_key")
|
||||
s3sec.NewKey("secret_key", "secret_key")
|
||||
@@ -36,7 +38,8 @@ func TestImageUploaderFactory(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("with bucket url https://s3.amazonaws.com/mybucket", func() {
|
||||
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
|
||||
s3sec, err := setting.Raw.GetSection("external_image_storage.s3")
|
||||
So(err, ShouldBeNil)
|
||||
s3sec.NewKey("bucket_url", "https://s3.amazonaws.com/my.bucket.com")
|
||||
s3sec.NewKey("access_key", "access_key")
|
||||
s3sec.NewKey("secret_key", "secret_key")
|
||||
@@ -54,16 +57,16 @@ func TestImageUploaderFactory(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("with bucket url https://s3-us-west-2.amazonaws.com/mybucket", func() {
|
||||
s3sec, err := setting.Cfg.GetSection("external_image_storage.s3")
|
||||
s3sec, err := setting.Raw.GetSection("external_image_storage.s3")
|
||||
So(err, ShouldBeNil)
|
||||
s3sec.NewKey("bucket_url", "https://s3-us-west-2.amazonaws.com/my.bucket.com")
|
||||
s3sec.NewKey("access_key", "access_key")
|
||||
s3sec.NewKey("secret_key", "secret_key")
|
||||
|
||||
uploader, err := NewImageUploader()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
original, ok := uploader.(*S3Uploader)
|
||||
|
||||
original, ok := uploader.(*S3Uploader)
|
||||
So(ok, ShouldBeTrue)
|
||||
So(original.region, ShouldEqual, "us-west-2")
|
||||
So(original.bucket, ShouldEqual, "my.bucket.com")
|
||||
@@ -75,13 +78,15 @@ func TestImageUploaderFactory(t *testing.T) {
|
||||
Convey("Webdav uploader", func() {
|
||||
var err error
|
||||
|
||||
setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
cfg := setting.NewCfg()
|
||||
cfg.Load(&setting.CommandLineArgs{
|
||||
HomePath: "../../../",
|
||||
})
|
||||
|
||||
setting.ImageUploadProvider = "webdav"
|
||||
|
||||
webdavSec, err := setting.Cfg.GetSection("external_image_storage.webdav")
|
||||
webdavSec, err := cfg.Raw.GetSection("external_image_storage.webdav")
|
||||
So(err, ShouldBeNil)
|
||||
webdavSec.NewKey("url", "webdavUrl")
|
||||
webdavSec.NewKey("username", "username")
|
||||
webdavSec.NewKey("password", "password")
|
||||
@@ -100,43 +105,45 @@ func TestImageUploaderFactory(t *testing.T) {
|
||||
Convey("GCS uploader", func() {
|
||||
var err error
|
||||
|
||||
setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
cfg := setting.NewCfg()
|
||||
cfg.Load(&setting.CommandLineArgs{
|
||||
HomePath: "../../../",
|
||||
})
|
||||
|
||||
setting.ImageUploadProvider = "gcs"
|
||||
|
||||
gcpSec, err := setting.Cfg.GetSection("external_image_storage.gcs")
|
||||
gcpSec, err := cfg.Raw.GetSection("external_image_storage.gcs")
|
||||
So(err, ShouldBeNil)
|
||||
gcpSec.NewKey("key_file", "/etc/secrets/project-79a52befa3f6.json")
|
||||
gcpSec.NewKey("bucket", "project-grafana-east")
|
||||
|
||||
uploader, err := NewImageUploader()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
original, ok := uploader.(*GCSUploader)
|
||||
|
||||
original, ok := uploader.(*GCSUploader)
|
||||
So(ok, ShouldBeTrue)
|
||||
So(original.keyFile, ShouldEqual, "/etc/secrets/project-79a52befa3f6.json")
|
||||
So(original.bucket, ShouldEqual, "project-grafana-east")
|
||||
})
|
||||
|
||||
Convey("AzureBlobUploader config", func() {
|
||||
setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
cfg := setting.NewCfg()
|
||||
cfg.Load(&setting.CommandLineArgs{
|
||||
HomePath: "../../../",
|
||||
})
|
||||
setting.ImageUploadProvider = "azure_blob"
|
||||
|
||||
Convey("with container name", func() {
|
||||
azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob")
|
||||
azureBlobSec, err := cfg.Raw.GetSection("external_image_storage.azure_blob")
|
||||
So(err, ShouldBeNil)
|
||||
azureBlobSec.NewKey("account_name", "account_name")
|
||||
azureBlobSec.NewKey("account_key", "account_key")
|
||||
azureBlobSec.NewKey("container_name", "container_name")
|
||||
|
||||
uploader, err := NewImageUploader()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
original, ok := uploader.(*AzureBlobUploader)
|
||||
|
||||
original, ok := uploader.(*AzureBlobUploader)
|
||||
So(ok, ShouldBeTrue)
|
||||
So(original.account_name, ShouldEqual, "account_name")
|
||||
So(original.account_key, ShouldEqual, "account_key")
|
||||
@@ -147,7 +154,8 @@ func TestImageUploaderFactory(t *testing.T) {
|
||||
Convey("Local uploader", func() {
|
||||
var err error
|
||||
|
||||
setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
cfg := setting.NewCfg()
|
||||
cfg.Load(&setting.CommandLineArgs{
|
||||
HomePath: "../../../",
|
||||
})
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
|
||||
func TestUploadToS3(t *testing.T) {
|
||||
SkipConvey("[Integration test] for external_image_store.s3", t, func() {
|
||||
setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
cfg := setting.NewCfg()
|
||||
cfg.Load(&setting.CommandLineArgs{
|
||||
HomePath: "../../../",
|
||||
})
|
||||
|
||||
|
||||
@@ -41,14 +41,20 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error)
|
||||
url.Path = path.Join(url.Path, filename)
|
||||
|
||||
imgData, err := ioutil.ReadFile(pa)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", url.String(), bytes.NewReader(imgData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if u.username != "" {
|
||||
req.SetBasicAuth(u.username, u.password)
|
||||
}
|
||||
|
||||
res, err := netClient.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func (f *Float) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
f.Float64 = float64(x)
|
||||
f.Float64 = x
|
||||
case map[string]interface{}:
|
||||
err = json.Unmarshal(data, &f.NullFloat64)
|
||||
case nil:
|
||||
@@ -106,6 +106,15 @@ func (f Float) String() string {
|
||||
return fmt.Sprintf("%1.3f", f.Float64)
|
||||
}
|
||||
|
||||
// FullString returns float as string in full precision
|
||||
func (f Float) FullString() string {
|
||||
if !f.Valid {
|
||||
return "null"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%f", f.Float64)
|
||||
}
|
||||
|
||||
// SetValid changes this Float's value and also sets it to be non-null.
|
||||
func (f *Float) SetValid(n float64) {
|
||||
f.Float64 = n
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package extensions
|
||||
|
||||
import _ "github.com/pkg/errors"
|
||||
+1
-4
@@ -99,10 +99,7 @@ func (w *FileLogWriter) StartLogger() error {
|
||||
return err
|
||||
}
|
||||
w.mw.SetFd(fd)
|
||||
if err = w.initFd(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return w.initFd()
|
||||
}
|
||||
|
||||
func (w *FileLogWriter) docheck(size int) {
|
||||
|
||||
@@ -32,7 +32,9 @@ func TestLogFile(t *testing.T) {
|
||||
|
||||
Convey("Logging should add lines", func() {
|
||||
err := fileLogWrite.WriteLine("test1\n")
|
||||
So(err, ShouldBeNil)
|
||||
err = fileLogWrite.WriteLine("test2\n")
|
||||
So(err, ShouldBeNil)
|
||||
err = fileLogWrite.WriteLine("test3\n")
|
||||
So(err, ShouldBeNil)
|
||||
So(fileLogWrite.maxlines_curlines, ShouldEqual, 3)
|
||||
|
||||
+9
-10
@@ -8,23 +8,22 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidCredentials = errors.New("Invalid Username or Password")
|
||||
ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
|
||||
ErrEmailNotAllowed = errors.New("Required email domain not fulfilled")
|
||||
ErrInvalidCredentials = errors.New("Invalid Username or Password")
|
||||
ErrNoEmail = errors.New("Login provider didn't return an email address")
|
||||
ErrProviderDeniedRequest = errors.New("Login provider denied login request")
|
||||
ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter")
|
||||
ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked")
|
||||
ErrUsersQuotaReached = errors.New("Users quota reached")
|
||||
ErrGettingUserQuota = errors.New("Error getting user quota")
|
||||
)
|
||||
|
||||
type LoginUserQuery struct {
|
||||
Username string
|
||||
Password string
|
||||
User *m.User
|
||||
IpAddress string
|
||||
}
|
||||
|
||||
func Init() {
|
||||
bus.AddHandler("auth", AuthenticateUser)
|
||||
loadLdapConfig()
|
||||
}
|
||||
|
||||
func AuthenticateUser(query *LoginUserQuery) error {
|
||||
func AuthenticateUser(query *m.LoginUserQuery) error {
|
||||
if err := validateLoginAttempts(query.Username); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ func TestAuthenticateUser(t *testing.T) {
|
||||
}
|
||||
|
||||
type authScenarioContext struct {
|
||||
loginUserQuery *LoginUserQuery
|
||||
loginUserQuery *m.LoginUserQuery
|
||||
grafanaLoginWasCalled bool
|
||||
ldapLoginWasCalled bool
|
||||
loginAttemptValidationWasCalled bool
|
||||
@@ -161,14 +161,14 @@ type authScenarioContext struct {
|
||||
type authScenarioFunc func(sc *authScenarioContext)
|
||||
|
||||
func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) {
|
||||
loginUsingGrafanaDB = func(query *LoginUserQuery) error {
|
||||
loginUsingGrafanaDB = func(query *m.LoginUserQuery) error {
|
||||
sc.grafanaLoginWasCalled = true
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func mockLoginUsingLdap(enabled bool, err error, sc *authScenarioContext) {
|
||||
loginUsingLdap = func(query *LoginUserQuery) (bool, error) {
|
||||
loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) {
|
||||
sc.ldapLoginWasCalled = true
|
||||
return enabled, err
|
||||
}
|
||||
@@ -182,7 +182,7 @@ func mockLoginAttemptValidation(err error, sc *authScenarioContext) {
|
||||
}
|
||||
|
||||
func mockSaveInvalidLoginAttempt(sc *authScenarioContext) {
|
||||
saveInvalidLoginAttempt = func(query *LoginUserQuery) {
|
||||
saveInvalidLoginAttempt = func(query *m.LoginUserQuery) {
|
||||
sc.saveInvalidLoginAttemptWasCalled = true
|
||||
}
|
||||
}
|
||||
@@ -195,7 +195,7 @@ func authScenario(desc string, fn authScenarioFunc) {
|
||||
origSaveInvalidLoginAttempt := saveInvalidLoginAttempt
|
||||
|
||||
sc := &authScenarioContext{
|
||||
loginUserQuery: &LoginUserQuery{
|
||||
loginUserQuery: &m.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
maxInvalidLoginAttempts int64 = 5
|
||||
loginAttemptsWindow time.Duration = time.Minute * 5
|
||||
maxInvalidLoginAttempts int64 = 5
|
||||
loginAttemptsWindow = time.Minute * 5
|
||||
)
|
||||
|
||||
var validateLoginAttempts = func(username string) error {
|
||||
@@ -34,7 +34,7 @@ var validateLoginAttempts = func(username string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var saveInvalidLoginAttempt = func(query *LoginUserQuery) {
|
||||
var saveInvalidLoginAttempt = func(query *m.LoginUserQuery) {
|
||||
if setting.DisableBruteForceLoginProtection {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
saveInvalidLoginAttempt(&LoginUserQuery{
|
||||
saveInvalidLoginAttempt(&m.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
@@ -103,7 +103,7 @@ func TestLoginAttemptsValidation(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
saveInvalidLoginAttempt(&LoginUserQuery{
|
||||
saveInvalidLoginAttempt(&m.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("auth", UpsertUser)
|
||||
}
|
||||
|
||||
func UpsertUser(cmd *m.UpsertUserCommand) error {
|
||||
extUser := cmd.ExternalUser
|
||||
|
||||
userQuery := &m.GetUserByAuthInfoQuery{
|
||||
AuthModule: extUser.AuthModule,
|
||||
AuthId: extUser.AuthId,
|
||||
UserId: extUser.UserId,
|
||||
Email: extUser.Email,
|
||||
Login: extUser.Login,
|
||||
}
|
||||
err := bus.Dispatch(userQuery)
|
||||
if err != m.ErrUserNotFound && err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if !cmd.SignupAllowed {
|
||||
log.Warn("Not allowing %s login, user not found in internal user database and allow signup = false", extUser.AuthModule)
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
|
||||
limitReached, err := quota.QuotaReached(cmd.ReqContext, "user")
|
||||
if err != nil {
|
||||
log.Warn("Error getting user quota", "err", err)
|
||||
return ErrGettingUserQuota
|
||||
}
|
||||
if limitReached {
|
||||
return ErrUsersQuotaReached
|
||||
}
|
||||
|
||||
cmd.Result, err = createUser(extUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if extUser.AuthModule != "" && extUser.AuthId != "" {
|
||||
cmd2 := &m.SetAuthInfoCommand{
|
||||
UserId: cmd.Result.Id,
|
||||
AuthModule: extUser.AuthModule,
|
||||
AuthId: extUser.AuthId,
|
||||
}
|
||||
if err := bus.Dispatch(cmd2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
cmd.Result = userQuery.Result
|
||||
|
||||
err = updateUser(cmd.Result, extUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return syncOrgRoles(cmd.Result, extUser)
|
||||
}
|
||||
|
||||
func createUser(extUser *m.ExternalUserInfo) (*m.User, error) {
|
||||
cmd := &m.CreateUserCommand{
|
||||
Login: extUser.Login,
|
||||
Email: extUser.Email,
|
||||
Name: extUser.Name,
|
||||
SkipOrgSetup: len(extUser.OrgRoles) > 0,
|
||||
}
|
||||
if err := bus.Dispatch(cmd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cmd.Result, nil
|
||||
}
|
||||
|
||||
func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
|
||||
// sync user info
|
||||
updateCmd := &m.UpdateUserCommand{
|
||||
UserId: user.Id,
|
||||
}
|
||||
|
||||
needsUpdate := false
|
||||
if extUser.Login != "" && extUser.Login != user.Login {
|
||||
updateCmd.Login = extUser.Login
|
||||
user.Login = extUser.Login
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if extUser.Email != "" && extUser.Email != user.Email {
|
||||
updateCmd.Email = extUser.Email
|
||||
user.Email = extUser.Email
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if extUser.Name != "" && extUser.Name != user.Name {
|
||||
updateCmd.Name = extUser.Name
|
||||
user.Name = extUser.Name
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if !needsUpdate {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug("Syncing user info", "id", user.Id, "update", updateCmd)
|
||||
return bus.Dispatch(updateCmd)
|
||||
}
|
||||
|
||||
func syncOrgRoles(user *m.User, extUser *m.ExternalUserInfo) error {
|
||||
// don't sync org roles if none are specified
|
||||
if len(extUser.OrgRoles) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
orgsQuery := &m.GetUserOrgListQuery{UserId: user.Id}
|
||||
if err := bus.Dispatch(orgsQuery); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handledOrgIds := map[int64]bool{}
|
||||
deleteOrgIds := []int64{}
|
||||
|
||||
// update existing org roles
|
||||
for _, org := range orgsQuery.Result {
|
||||
handledOrgIds[org.OrgId] = true
|
||||
|
||||
if extUser.OrgRoles[org.OrgId] == "" {
|
||||
deleteOrgIds = append(deleteOrgIds, org.OrgId)
|
||||
} else if extUser.OrgRoles[org.OrgId] != org.Role {
|
||||
// update role
|
||||
cmd := &m.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: extUser.OrgRoles[org.OrgId]}
|
||||
if err := bus.Dispatch(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add any new org roles
|
||||
for orgId, orgRole := range extUser.OrgRoles {
|
||||
if _, exists := handledOrgIds[orgId]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// add role
|
||||
cmd := &m.AddOrgUserCommand{UserId: user.Id, Role: orgRole, OrgId: orgId}
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil && err != m.ErrOrgNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// delete any removed org roles
|
||||
for _, orgId := range deleteOrgIds {
|
||||
cmd := &m.RemoveOrgUserCommand{OrgId: orgId, UserId: user.Id}
|
||||
if err := bus.Dispatch(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update user's default org if needed
|
||||
if _, ok := extUser.OrgRoles[user.OrgId]; !ok {
|
||||
for orgId := range extUser.OrgRoles {
|
||||
user.OrgId = orgId
|
||||
break
|
||||
}
|
||||
|
||||
return bus.Dispatch(&m.SetUsingOrgCommand{
|
||||
UserId: user.Id,
|
||||
OrgId: user.OrgId,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -17,7 +17,7 @@ var validatePassword = func(providedPassword string, userPassword string, userSa
|
||||
return nil
|
||||
}
|
||||
|
||||
var loginUsingGrafanaDB = func(query *LoginUserQuery) error {
|
||||
var loginUsingGrafanaDB = func(query *m.LoginUserQuery) error {
|
||||
userQuery := m.GetUserByLoginQuery{LoginOrEmail: query.Username}
|
||||
|
||||
if err := bus.Dispatch(&userQuery); err != nil {
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestGrafanaLogin(t *testing.T) {
|
||||
}
|
||||
|
||||
type grafanaLoginScenarioContext struct {
|
||||
loginUserQuery *LoginUserQuery
|
||||
loginUserQuery *m.LoginUserQuery
|
||||
validatePasswordCalled bool
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func grafanaLoginScenario(desc string, fn grafanaLoginScenarioFunc) {
|
||||
origValidatePassword := validatePassword
|
||||
|
||||
sc := &grafanaLoginScenarioContext{
|
||||
loginUserQuery: &LoginUserQuery{
|
||||
loginUserQuery: &m.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
|
||||
+84
-185
@@ -24,10 +24,9 @@ type ILdapConn interface {
|
||||
}
|
||||
|
||||
type ILdapAuther interface {
|
||||
Login(query *LoginUserQuery) error
|
||||
SyncSignedInUser(signedInUser *m.SignedInUser) error
|
||||
GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error)
|
||||
SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error
|
||||
Login(query *m.LoginUserQuery) error
|
||||
SyncUser(query *m.LoginUserQuery) error
|
||||
GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error)
|
||||
}
|
||||
|
||||
type ldapAuther struct {
|
||||
@@ -51,12 +50,12 @@ func (a *ldapAuther) Dial() error {
|
||||
if a.server.RootCACert != "" {
|
||||
certPool = x509.NewCertPool()
|
||||
for _, caCertFile := range strings.Split(a.server.RootCACert, " ") {
|
||||
if pem, err := ioutil.ReadFile(caCertFile); err != nil {
|
||||
pem, err := ioutil.ReadFile(caCertFile)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
if !certPool.AppendCertsFromPEM(pem) {
|
||||
return errors.New("Failed to append CA certificate " + caCertFile)
|
||||
}
|
||||
}
|
||||
if !certPool.AppendCertsFromPEM(pem) {
|
||||
return errors.New("Failed to append CA certificate " + caCertFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +88,8 @@ func (a *ldapAuther) Dial() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *ldapAuther) Login(query *LoginUserQuery) error {
|
||||
func (a *ldapAuther) Login(query *m.LoginUserQuery) error {
|
||||
// connect to ldap server
|
||||
if err := a.Dial(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -101,206 +101,105 @@ func (a *ldapAuther) Login(query *LoginUserQuery) error {
|
||||
}
|
||||
|
||||
// find user entry & attributes
|
||||
if ldapUser, err := a.searchForUser(query.Username); err != nil {
|
||||
ldapUser, err := a.searchForUser(query.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
|
||||
}
|
||||
|
||||
// check if a second user bind is needed
|
||||
if a.requireSecondBind {
|
||||
if err := a.secondBind(ldapUser, query.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
|
||||
|
||||
if grafanaUser, err := a.GetGrafanaUserFor(ldapUser); err != nil {
|
||||
// check if a second user bind is needed
|
||||
if a.requireSecondBind {
|
||||
err = a.secondBind(ldapUser, query.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
if syncErr := a.syncInfoAndOrgRoles(grafanaUser, ldapUser); syncErr != nil {
|
||||
return syncErr
|
||||
}
|
||||
query.User = grafanaUser
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query.User = grafanaUser
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error {
|
||||
grafanaUser := m.User{
|
||||
Id: signedInUser.UserId,
|
||||
Login: signedInUser.Login,
|
||||
Email: signedInUser.Email,
|
||||
Name: signedInUser.Name,
|
||||
}
|
||||
|
||||
if err := a.Dial(); err != nil {
|
||||
func (a *ldapAuther) SyncUser(query *m.LoginUserQuery) error {
|
||||
// connect to ldap server
|
||||
err := a.Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer a.conn.Close()
|
||||
if err := a.serverBind(); err != nil {
|
||||
|
||||
err = a.serverBind()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ldapUser, err := a.searchForUser(signedInUser.Login); err != nil {
|
||||
// find user entry & attributes
|
||||
ldapUser, err := a.searchForUser(query.Username)
|
||||
if err != nil {
|
||||
a.log.Error("Failed searching for user in ldap", "error", err)
|
||||
|
||||
return err
|
||||
} else {
|
||||
if err := a.syncInfoAndOrgRoles(&grafanaUser, ldapUser); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
|
||||
|
||||
grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query.User = grafanaUser
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) {
|
||||
extUser := &m.ExternalUserInfo{
|
||||
AuthModule: "ldap",
|
||||
AuthId: ldapUser.DN,
|
||||
Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName),
|
||||
Login: ldapUser.Username,
|
||||
Email: ldapUser.Email,
|
||||
OrgRoles: map[int64]m.RoleType{},
|
||||
}
|
||||
|
||||
for _, group := range a.server.LdapGroups {
|
||||
// only use the first match for each org
|
||||
if extUser.OrgRoles[group.OrgId] != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
a.log.Debug("Got Ldap User Info", "user", spew.Sdump(ldapUser))
|
||||
if ldapUser.isMemberOf(group.GroupDN) {
|
||||
extUser.OrgRoles[group.OrgId] = group.OrgRole
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sync info for ldap user and grafana user
|
||||
func (a *ldapAuther) syncInfoAndOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
|
||||
// sync user details
|
||||
if err := a.syncUserInfo(user, ldapUser); err != nil {
|
||||
return err
|
||||
}
|
||||
// sync org roles
|
||||
if err := a.SyncOrgRoles(user, ldapUser); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) {
|
||||
// validate that the user has access
|
||||
// if there are no ldap group mappings access is true
|
||||
// otherwise a single group must match
|
||||
access := len(a.server.LdapGroups) == 0
|
||||
for _, ldapGroup := range a.server.LdapGroups {
|
||||
if ldapUser.isMemberOf(ldapGroup.GroupDN) {
|
||||
access = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !access {
|
||||
a.log.Info("Ldap Auth: user does not belong in any of the specified ldap groups", "username", ldapUser.Username, "groups", ldapUser.MemberOf)
|
||||
if len(a.server.LdapGroups) > 0 && len(extUser.OrgRoles) < 1 {
|
||||
a.log.Info(
|
||||
"Ldap Auth: user does not belong in any of the specified ldap groups",
|
||||
"username", ldapUser.Username,
|
||||
"groups", ldapUser.MemberOf)
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// get user from grafana db
|
||||
userQuery := m.GetUserByLoginQuery{LoginOrEmail: ldapUser.Username}
|
||||
if err := bus.Dispatch(&userQuery); err != nil {
|
||||
if err == m.ErrUserNotFound && setting.LdapAllowSignup {
|
||||
return a.createGrafanaUser(ldapUser)
|
||||
} else if err == m.ErrUserNotFound {
|
||||
a.log.Warn("Not allowing LDAP login, user not found in internal user database, and ldap allow signup = false")
|
||||
return nil, ErrInvalidCredentials
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
// add/update user in grafana
|
||||
userQuery := &m.UpsertUserCommand{
|
||||
ReqContext: ctx,
|
||||
ExternalUser: extUser,
|
||||
SignupAllowed: setting.LdapAllowSignup,
|
||||
}
|
||||
|
||||
return userQuery.Result, nil
|
||||
|
||||
}
|
||||
func (a *ldapAuther) createGrafanaUser(ldapUser *LdapUserInfo) (*m.User, error) {
|
||||
cmd := m.CreateUserCommand{
|
||||
Login: ldapUser.Username,
|
||||
Email: ldapUser.Email,
|
||||
Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName),
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
err := bus.Dispatch(userQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cmd.Result, nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) syncUserInfo(user *m.User, ldapUser *LdapUserInfo) error {
|
||||
var name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName)
|
||||
if user.Email == ldapUser.Email && user.Name == name {
|
||||
return nil
|
||||
}
|
||||
|
||||
a.log.Debug("Syncing user info", "username", ldapUser.Username)
|
||||
updateCmd := m.UpdateUserCommand{}
|
||||
updateCmd.UserId = user.Id
|
||||
updateCmd.Login = user.Login
|
||||
updateCmd.Email = ldapUser.Email
|
||||
updateCmd.Name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName)
|
||||
return bus.Dispatch(&updateCmd)
|
||||
}
|
||||
|
||||
func (a *ldapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
|
||||
if len(a.server.LdapGroups) == 0 {
|
||||
a.log.Warn("No group mappings defined")
|
||||
return nil
|
||||
}
|
||||
|
||||
orgsQuery := m.GetUserOrgListQuery{UserId: user.Id}
|
||||
if err := bus.Dispatch(&orgsQuery); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handledOrgIds := map[int64]bool{}
|
||||
|
||||
// update or remove org roles
|
||||
for _, org := range orgsQuery.Result {
|
||||
match := false
|
||||
handledOrgIds[org.OrgId] = true
|
||||
|
||||
for _, group := range a.server.LdapGroups {
|
||||
if org.OrgId != group.OrgId {
|
||||
continue
|
||||
}
|
||||
|
||||
if ldapUser.isMemberOf(group.GroupDN) {
|
||||
match = true
|
||||
if org.Role != group.OrgRole {
|
||||
// update role
|
||||
cmd := m.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: group.OrgRole}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// ignore subsequent ldap group mapping matches
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// remove role if no mappings match
|
||||
if !match {
|
||||
cmd := m.RemoveOrgUserCommand{OrgId: org.OrgId, UserId: user.Id}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add missing org roles
|
||||
for _, group := range a.server.LdapGroups {
|
||||
if !ldapUser.isMemberOf(group.GroupDN) {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, exists := handledOrgIds[group.OrgId]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// add role
|
||||
cmd := m.AddOrgUserCommand{UserId: user.Id, Role: group.OrgRole, OrgId: group.OrgId}
|
||||
err := bus.Dispatch(&cmd)
|
||||
if err != nil && err != m.ErrOrgNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
// mark this group has handled so we do not process it again
|
||||
handledOrgIds[group.OrgId] = true
|
||||
}
|
||||
|
||||
return nil
|
||||
return userQuery.Result, nil
|
||||
}
|
||||
|
||||
func (a *ldapAuther) serverBind() error {
|
||||
@@ -404,9 +303,10 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
|
||||
var groupSearchResult *ldap.SearchResult
|
||||
for _, groupSearchBase := range a.server.GroupSearchBaseDNs {
|
||||
var filter_replace string
|
||||
filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
|
||||
if a.server.GroupSearchFilterUserAttribute == "" {
|
||||
filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
|
||||
} else {
|
||||
filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
|
||||
}
|
||||
filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1)
|
||||
|
||||
@@ -448,6 +348,9 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
|
||||
}
|
||||
|
||||
func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
|
||||
if name == "DN" {
|
||||
return result.Entries[0].DN
|
||||
}
|
||||
for _, attr := range result.Entries[n].Attributes {
|
||||
if attr.Name == name {
|
||||
if len(attr.Values) > 0 {
|
||||
@@ -470,7 +373,3 @@ func getLdapAttrArray(name string, result *ldap.SearchResult) []string {
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func createUserFromLdapInfo() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var loginUsingLdap = func(query *LoginUserQuery) (bool, error) {
|
||||
var loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) {
|
||||
if !setting.LdapEnabled {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func TestLdapLogin(t *testing.T) {
|
||||
|
||||
ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) {
|
||||
sc.withLoginResult(false)
|
||||
enabled, err := loginUsingLdap(&LoginUserQuery{
|
||||
enabled, err := loginUsingLdap(&m.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
})
|
||||
@@ -117,7 +117,7 @@ type mockLdapAuther struct {
|
||||
loginCalled bool
|
||||
}
|
||||
|
||||
func (a *mockLdapAuther) Login(query *LoginUserQuery) error {
|
||||
func (a *mockLdapAuther) Login(query *m.LoginUserQuery) error {
|
||||
a.loginCalled = true
|
||||
|
||||
if !a.validLogin {
|
||||
@@ -127,20 +127,16 @@ func (a *mockLdapAuther) Login(query *LoginUserQuery) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *mockLdapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error {
|
||||
func (a *mockLdapAuther) SyncUser(query *m.LoginUserQuery) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *mockLdapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) {
|
||||
func (a *mockLdapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *mockLdapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ldapLoginScenarioContext struct {
|
||||
loginUserQuery *LoginUserQuery
|
||||
loginUserQuery *m.LoginUserQuery
|
||||
ldapAuthenticatorMock *mockLdapAuther
|
||||
}
|
||||
|
||||
@@ -151,7 +147,7 @@ func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) {
|
||||
origNewLdapAuthenticator := NewLdapAuthenticator
|
||||
|
||||
sc := &ldapLoginScenarioContext{
|
||||
loginUserQuery: &LoginUserQuery{
|
||||
loginUserQuery: &m.LoginUserQuery{
|
||||
Username: "user",
|
||||
Password: "pwd",
|
||||
IpAddress: "192.168.1.1:56433",
|
||||
|
||||
+78
-34
@@ -18,7 +18,7 @@ func TestLdapAuther(t *testing.T) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{{}},
|
||||
})
|
||||
_, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
|
||||
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{})
|
||||
|
||||
So(err, ShouldEqual, ErrInvalidCredentials)
|
||||
})
|
||||
@@ -34,7 +34,7 @@ func TestLdapAuther(t *testing.T) {
|
||||
|
||||
sc.userQueryReturns(user1)
|
||||
|
||||
result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{})
|
||||
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{})
|
||||
So(err, ShouldBeNil)
|
||||
So(result, ShouldEqual, user1)
|
||||
})
|
||||
@@ -48,7 +48,21 @@ func TestLdapAuther(t *testing.T) {
|
||||
|
||||
sc.userQueryReturns(user1)
|
||||
|
||||
result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{MemberOf: []string{"cn=users"}})
|
||||
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{MemberOf: []string{"cn=users"}})
|
||||
So(err, ShouldBeNil)
|
||||
So(result, ShouldEqual, user1)
|
||||
})
|
||||
|
||||
ldapAutherScenario("Given group match with different case", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
{GroupDN: "cn=users", OrgRole: "Admin"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userQueryReturns(user1)
|
||||
|
||||
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{MemberOf: []string{"CN=users"}})
|
||||
So(err, ShouldBeNil)
|
||||
So(result, ShouldEqual, user1)
|
||||
})
|
||||
@@ -64,7 +78,8 @@ func TestLdapAuther(t *testing.T) {
|
||||
|
||||
sc.userQueryReturns(nil)
|
||||
|
||||
result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{
|
||||
result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
DN: "torkelo",
|
||||
Username: "torkelo",
|
||||
Email: "my@email.com",
|
||||
MemberOf: []string{"cn=editor"},
|
||||
@@ -72,11 +87,6 @@ func TestLdapAuther(t *testing.T) {
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should create new user", func() {
|
||||
So(sc.createUserCmd.Login, ShouldEqual, "torkelo")
|
||||
So(sc.createUserCmd.Email, ShouldEqual, "my@email.com")
|
||||
})
|
||||
|
||||
Convey("Should return new user", func() {
|
||||
So(result.Login, ShouldEqual, "torkelo")
|
||||
})
|
||||
@@ -95,7 +105,7 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
|
||||
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
MemberOf: []string{"cn=users"},
|
||||
})
|
||||
|
||||
@@ -114,7 +124,7 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
|
||||
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
|
||||
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
MemberOf: []string{"cn=users"},
|
||||
})
|
||||
|
||||
@@ -122,24 +132,29 @@ func TestLdapAuther(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.updateOrgUserCmd, ShouldNotBeNil)
|
||||
So(sc.updateOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
|
||||
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
|
||||
ldapAutherScenario("given current org role is removed in ldap", func(sc *scenarioContext) {
|
||||
ldapAuther := NewLdapAuthenticator(&LdapServerConf{
|
||||
LdapGroups: []*LdapGroupToOrgRole{
|
||||
{GroupDN: "cn=users", OrgId: 1, OrgRole: "Admin"},
|
||||
{GroupDN: "cn=users", OrgId: 2, OrgRole: "Admin"},
|
||||
},
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
|
||||
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
|
||||
MemberOf: []string{"cn=other"},
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{
|
||||
{OrgId: 1, Role: m.ROLE_EDITOR},
|
||||
{OrgId: 2, Role: m.ROLE_EDITOR},
|
||||
})
|
||||
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
MemberOf: []string{"cn=users"},
|
||||
})
|
||||
|
||||
Convey("Should remove org role", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.removeOrgUserCmd, ShouldNotBeNil)
|
||||
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -152,7 +167,7 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}})
|
||||
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
|
||||
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
MemberOf: []string{"cn=users"},
|
||||
})
|
||||
|
||||
@@ -160,6 +175,7 @@ func TestLdapAuther(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.removeOrgUserCmd, ShouldBeNil)
|
||||
So(sc.updateOrgUserCmd, ShouldNotBeNil)
|
||||
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -172,13 +188,14 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_ADMIN}})
|
||||
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
|
||||
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
MemberOf: []string{"cn=admins"},
|
||||
})
|
||||
|
||||
Convey("Should take first match, and ignore subsequent matches", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.updateOrgUserCmd, ShouldBeNil)
|
||||
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -191,19 +208,20 @@ func TestLdapAuther(t *testing.T) {
|
||||
})
|
||||
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||
err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{
|
||||
_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
|
||||
MemberOf: []string{"cn=admins"},
|
||||
})
|
||||
|
||||
Convey("Should take first match, and ignore subsequent matches", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
|
||||
So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Convey("When calling SyncSignedInUser", t, func() {
|
||||
Convey("When calling SyncUser", t, func() {
|
||||
|
||||
mockLdapConnection := &mockLdapConn{}
|
||||
ldapAuther := NewLdapAuthenticator(
|
||||
@@ -243,17 +261,20 @@ func TestLdapAuther(t *testing.T) {
|
||||
|
||||
ldapAutherScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) {
|
||||
// arrange
|
||||
signedInUser := &m.SignedInUser{
|
||||
Email: "roel@test.net",
|
||||
UserId: 1,
|
||||
Name: "Roel Gerrits",
|
||||
Login: "roelgerrits",
|
||||
query := &m.LoginUserQuery{
|
||||
Username: "roelgerrits",
|
||||
}
|
||||
|
||||
sc.userQueryReturns(&m.User{
|
||||
Id: 1,
|
||||
Email: "roel@test.net",
|
||||
Name: "Roel Gerrits",
|
||||
Login: "roelgerrits",
|
||||
})
|
||||
sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
|
||||
|
||||
// act
|
||||
syncErrResult := ldapAuther.SyncSignedInUser(signedInUser)
|
||||
syncErrResult := ldapAuther.SyncUser(query)
|
||||
|
||||
// assert
|
||||
So(dialCalled, ShouldBeTrue)
|
||||
@@ -299,6 +320,19 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
|
||||
|
||||
sc := &scenarioContext{}
|
||||
|
||||
bus.AddHandler("test", UpsertUser)
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
|
||||
sc.getUserByAuthInfoQuery = cmd
|
||||
sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.GetUserOrgListQuery) error {
|
||||
sc.getUserOrgListQuery = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.CreateUserCommand) error {
|
||||
sc.createUserCmd = cmd
|
||||
sc.createUserCmd.Result = m.User{Login: cmd.Login}
|
||||
@@ -325,26 +359,36 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.SetUsingOrgCommand) error {
|
||||
sc.setUsingOrgCmd = cmd
|
||||
return nil
|
||||
})
|
||||
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
type scenarioContext struct {
|
||||
createUserCmd *m.CreateUserCommand
|
||||
addOrgUserCmd *m.AddOrgUserCommand
|
||||
updateOrgUserCmd *m.UpdateOrgUserCommand
|
||||
removeOrgUserCmd *m.RemoveOrgUserCommand
|
||||
updateUserCmd *m.UpdateUserCommand
|
||||
getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery
|
||||
getUserOrgListQuery *m.GetUserOrgListQuery
|
||||
createUserCmd *m.CreateUserCommand
|
||||
addOrgUserCmd *m.AddOrgUserCommand
|
||||
updateOrgUserCmd *m.UpdateOrgUserCommand
|
||||
removeOrgUserCmd *m.RemoveOrgUserCommand
|
||||
updateUserCmd *m.UpdateUserCommand
|
||||
setUsingOrgCmd *m.SetUsingOrgCommand
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) userQueryReturns(user *m.User) {
|
||||
bus.AddHandler("test", func(query *m.GetUserByLoginQuery) error {
|
||||
bus.AddHandler("test", func(query *m.GetUserByAuthInfoQuery) error {
|
||||
if user == nil {
|
||||
return m.ErrUserNotFound
|
||||
} else {
|
||||
query.Result = user
|
||||
return nil
|
||||
}
|
||||
query.Result = user
|
||||
return nil
|
||||
})
|
||||
bus.AddHandler("test", func(query *m.SetAuthInfoCommand) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type LdapUserInfo struct {
|
||||
DN string
|
||||
FirstName string
|
||||
@@ -15,7 +19,7 @@ func (u *LdapUserInfo) isMemberOf(group string) bool {
|
||||
}
|
||||
|
||||
for _, member := range u.MemberOf {
|
||||
if member == group {
|
||||
if strings.EqualFold(member, group) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ const (
|
||||
AbortOnError
|
||||
)
|
||||
|
||||
var metricCategoryPrefix []string = []string{
|
||||
var metricCategoryPrefix = []string{
|
||||
"proxy_",
|
||||
"api_",
|
||||
"page_",
|
||||
@@ -66,7 +66,7 @@ var metricCategoryPrefix []string = []string{
|
||||
"go_",
|
||||
"process_"}
|
||||
|
||||
var trimMetricPrefix []string = []string{"grafana_"}
|
||||
var trimMetricPrefix = []string{"grafana_"}
|
||||
|
||||
// Config defines the Graphite bridge config.
|
||||
type Config struct {
|
||||
@@ -295,11 +295,7 @@ func writeMetric(buf *bufio.Writer, m model.Metric, mf *dto.MetricFamily) error
|
||||
}
|
||||
}
|
||||
|
||||
if err = addExtentionConventionForRollups(buf, mf, m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return addExtentionConventionForRollups(buf, mf, m)
|
||||
}
|
||||
|
||||
func addExtentionConventionForRollups(buf *bufio.Writer, mf *dto.MetricFamily, m model.Metric) error {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ini "gopkg.in/ini.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
|
||||
)
|
||||
|
||||
var metricsLogger log.Logger = log.New("metrics")
|
||||
|
||||
type logWrapper struct {
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (lw *logWrapper) Println(v ...interface{}) {
|
||||
lw.logger.Info("graphite metric bridge", v...)
|
||||
}
|
||||
|
||||
func Init(file *ini.File) {
|
||||
cfg := ReadSettings(file)
|
||||
internalInit(cfg)
|
||||
}
|
||||
|
||||
func internalInit(settings *MetricSettings) {
|
||||
initMetricVars(settings)
|
||||
|
||||
if settings.GraphiteBridgeConfig != nil {
|
||||
bridge, err := graphitebridge.NewBridge(settings.GraphiteBridgeConfig)
|
||||
if err != nil {
|
||||
metricsLogger.Error("failed to create graphite bridge", "error", err)
|
||||
} else {
|
||||
go bridge.Run(context.Background())
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-34
@@ -54,6 +54,7 @@ var (
|
||||
M_Alerting_Active_Alerts prometheus.Gauge
|
||||
M_StatTotal_Dashboards prometheus.Gauge
|
||||
M_StatTotal_Users prometheus.Gauge
|
||||
M_StatActive_Users prometheus.Gauge
|
||||
M_StatTotal_Orgs prometheus.Gauge
|
||||
M_StatTotal_Playlists prometheus.Gauge
|
||||
M_Grafana_Version *prometheus.GaugeVec
|
||||
@@ -253,6 +254,12 @@ func init() {
|
||||
Namespace: exporterName,
|
||||
})
|
||||
|
||||
M_StatActive_Users = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "stat_active_users",
|
||||
Help: "number of active users",
|
||||
Namespace: exporterName,
|
||||
})
|
||||
|
||||
M_StatTotal_Orgs = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "stat_total_orgs",
|
||||
Help: "total amount of orgs",
|
||||
@@ -270,10 +277,9 @@ func init() {
|
||||
Help: "Information about the Grafana",
|
||||
Namespace: exporterName,
|
||||
}, []string{"version"})
|
||||
|
||||
}
|
||||
|
||||
func initMetricVars(settings *MetricSettings) {
|
||||
func initMetricVars() {
|
||||
prometheus.MustRegister(
|
||||
M_Instance_Start,
|
||||
M_Page_Status,
|
||||
@@ -305,45 +311,25 @@ func initMetricVars(settings *MetricSettings) {
|
||||
M_Alerting_Active_Alerts,
|
||||
M_StatTotal_Dashboards,
|
||||
M_StatTotal_Users,
|
||||
M_StatActive_Users,
|
||||
M_StatTotal_Orgs,
|
||||
M_StatTotal_Playlists,
|
||||
M_Grafana_Version)
|
||||
|
||||
go instrumentationLoop(settings)
|
||||
}
|
||||
|
||||
func instrumentationLoop(settings *MetricSettings) chan struct{} {
|
||||
M_Instance_Start.Inc()
|
||||
|
||||
onceEveryDayTick := time.NewTicker(time.Hour * 24)
|
||||
secondTicker := time.NewTicker(time.Second * time.Duration(settings.IntervalSeconds))
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-onceEveryDayTick.C:
|
||||
sendUsageStats()
|
||||
case <-secondTicker.C:
|
||||
updateTotalStats()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var metricPublishCounter int64 = 0
|
||||
|
||||
func updateTotalStats() {
|
||||
metricPublishCounter++
|
||||
if metricPublishCounter == 1 || metricPublishCounter%10 == 0 {
|
||||
statsQuery := models.GetSystemStatsQuery{}
|
||||
if err := bus.Dispatch(&statsQuery); err != nil {
|
||||
metricsLogger.Error("Failed to get system stats", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
|
||||
M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
|
||||
M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
|
||||
M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
|
||||
statsQuery := models.GetSystemStatsQuery{}
|
||||
if err := bus.Dispatch(&statsQuery); err != nil {
|
||||
metricsLogger.Error("Failed to get system stats", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
M_StatTotal_Dashboards.Set(float64(statsQuery.Result.Dashboards))
|
||||
M_StatTotal_Users.Set(float64(statsQuery.Result.Users))
|
||||
M_StatActive_Users.Set(float64(statsQuery.Result.ActiveUsers))
|
||||
M_StatTotal_Playlists.Set(float64(statsQuery.Result.Playlists))
|
||||
M_StatTotal_Orgs.Set(float64(statsQuery.Result.Orgs))
|
||||
}
|
||||
|
||||
func sendUsageStats() {
|
||||
@@ -403,6 +389,6 @@ func sendUsageStats() {
|
||||
out, _ := json.MarshalIndent(report, "", " ")
|
||||
data := bytes.NewBuffer(out)
|
||||
|
||||
client := http.Client{Timeout: time.Duration(5 * time.Second)}
|
||||
client := http.Client{Timeout: 5 * time.Second}
|
||||
go client.Post("https://stats.grafana.org/grafana-usage-report", "application/json", data)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var metricsLogger log.Logger = log.New("metrics")
|
||||
|
||||
type logWrapper struct {
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
func (lw *logWrapper) Println(v ...interface{}) {
|
||||
lw.logger.Info("graphite metric bridge", v...)
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.RegisterService(&InternalMetricsService{})
|
||||
initMetricVars()
|
||||
}
|
||||
|
||||
type InternalMetricsService struct {
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
|
||||
enabled bool
|
||||
intervalSeconds int64
|
||||
graphiteCfg *graphitebridge.Config
|
||||
}
|
||||
|
||||
func (im *InternalMetricsService) Init() error {
|
||||
return im.readSettings()
|
||||
}
|
||||
|
||||
func (im *InternalMetricsService) Run(ctx context.Context) error {
|
||||
// Start Graphite Bridge
|
||||
if im.graphiteCfg != nil {
|
||||
bridge, err := graphitebridge.NewBridge(im.graphiteCfg)
|
||||
if err != nil {
|
||||
metricsLogger.Error("failed to create graphite bridge", "error", err)
|
||||
} else {
|
||||
go bridge.Run(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
M_Instance_Start.Inc()
|
||||
|
||||
// set the total stats gauges before we publishing metrics
|
||||
updateTotalStats()
|
||||
|
||||
onceEveryDayTick := time.NewTicker(time.Hour * 24)
|
||||
everyMinuteTicker := time.NewTicker(time.Minute)
|
||||
defer onceEveryDayTick.Stop()
|
||||
defer everyMinuteTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-onceEveryDayTick.C:
|
||||
sendUsageStats()
|
||||
case <-everyMinuteTicker.C:
|
||||
updateTotalStats()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-35
@@ -1,67 +1,53 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/metrics/graphitebridge"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
ini "gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
type MetricSettings struct {
|
||||
Enabled bool
|
||||
IntervalSeconds int64
|
||||
GraphiteBridgeConfig *graphitebridge.Config
|
||||
}
|
||||
|
||||
func ReadSettings(file *ini.File) *MetricSettings {
|
||||
var settings = &MetricSettings{
|
||||
Enabled: false,
|
||||
func (im *InternalMetricsService) readSettings() error {
|
||||
var section, err = im.Cfg.Raw.GetSection("metrics")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to find metrics config section %v", err)
|
||||
}
|
||||
|
||||
var section, err = file.GetSection("metrics")
|
||||
if err != nil {
|
||||
metricsLogger.Crit("Unable to find metrics config section", "error", err)
|
||||
im.enabled = section.Key("enabled").MustBool(false)
|
||||
im.intervalSeconds = section.Key("interval_seconds").MustInt64(10)
|
||||
|
||||
if !im.enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
settings.Enabled = section.Key("enabled").MustBool(false)
|
||||
settings.IntervalSeconds = section.Key("interval_seconds").MustInt64(10)
|
||||
|
||||
if !settings.Enabled {
|
||||
return settings
|
||||
if err := im.parseGraphiteSettings(); err != nil {
|
||||
return fmt.Errorf("Unable to parse metrics graphite section, %v", err)
|
||||
}
|
||||
|
||||
cfg, err := parseGraphiteSettings(settings, file)
|
||||
if err != nil {
|
||||
metricsLogger.Crit("Unable to parse metrics graphite section", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
settings.GraphiteBridgeConfig = cfg
|
||||
|
||||
return settings
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseGraphiteSettings(settings *MetricSettings, file *ini.File) (*graphitebridge.Config, error) {
|
||||
graphiteSection, err := setting.Cfg.GetSection("metrics.graphite")
|
||||
func (im *InternalMetricsService) parseGraphiteSettings() error {
|
||||
graphiteSection, err := im.Cfg.Raw.GetSection("metrics.graphite")
|
||||
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
address := graphiteSection.Key("address").String()
|
||||
if address == "" {
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg := &graphitebridge.Config{
|
||||
bridgeCfg := &graphitebridge.Config{
|
||||
URL: address,
|
||||
Prefix: graphiteSection.Key("prefix").MustString("prod.grafana.%(instance_name)s"),
|
||||
CountersAsDelta: true,
|
||||
Gatherer: prometheus.DefaultGatherer,
|
||||
Interval: time.Duration(settings.IntervalSeconds) * time.Second,
|
||||
Interval: time.Duration(im.intervalSeconds) * time.Second,
|
||||
Timeout: 10 * time.Second,
|
||||
Logger: &logWrapper{logger: metricsLogger},
|
||||
ErrorHandling: graphitebridge.ContinueOnError,
|
||||
@@ -74,6 +60,8 @@ func parseGraphiteSettings(settings *MetricSettings, file *ini.File) (*graphiteb
|
||||
prefix = "prod.grafana.%(instance_name)s."
|
||||
}
|
||||
|
||||
cfg.Prefix = strings.Replace(prefix, "%(instance_name)s", safeInstanceName, -1)
|
||||
return cfg, nil
|
||||
bridgeCfg.Prefix = strings.Replace(prefix, "%(instance_name)s", safeInstanceName, -1)
|
||||
|
||||
im.graphiteCfg = bridgeCfg
|
||||
return nil
|
||||
}
|
||||
|
||||
+127
-88
@@ -1,8 +1,10 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +16,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
var AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue"
|
||||
|
||||
func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
||||
if !setting.AuthProxyEnabled {
|
||||
return false
|
||||
@@ -25,45 +29,119 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
||||
}
|
||||
|
||||
// if auth proxy ip(s) defined, check if request comes from one of those
|
||||
if err := checkAuthenticationProxy(ctx, proxyHeaderValue); err != nil {
|
||||
if err := checkAuthenticationProxy(ctx.Req.RemoteAddr, proxyHeaderValue); err != nil {
|
||||
ctx.Handle(407, "Proxy authentication required", err)
|
||||
return true
|
||||
}
|
||||
|
||||
query := getSignedInUserQueryForProxyAuth(proxyHeaderValue)
|
||||
query.OrgId = orgID
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
if err != m.ErrUserNotFound {
|
||||
ctx.Handle(500, "Failed to find user specified in auth proxy header", err)
|
||||
return true
|
||||
}
|
||||
|
||||
if !setting.AuthProxyAutoSignUp {
|
||||
return false
|
||||
}
|
||||
|
||||
cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue)
|
||||
if setting.LdapEnabled {
|
||||
cmd.SkipOrgSetup = true
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(cmd); err != nil {
|
||||
ctx.Handle(500, "Failed to create user specified in auth proxy header", err)
|
||||
return true
|
||||
}
|
||||
query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id, OrgId: orgID}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
ctx.Handle(500, "Failed find user after creation", err)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// initialize session
|
||||
if err := ctx.Session.Start(ctx.Context); err != nil {
|
||||
log.Error(3, "Failed to start session", err)
|
||||
return false
|
||||
}
|
||||
|
||||
query := &m.GetSignedInUserQuery{OrgId: orgID}
|
||||
|
||||
// if this session has already been authenticated by authProxy just load the user
|
||||
sessProxyValue := ctx.Session.Get(AUTH_PROXY_SESSION_VAR)
|
||||
if sessProxyValue != nil && sessProxyValue.(string) == proxyHeaderValue && getRequestUserId(ctx) > 0 {
|
||||
// if we're using ldap, sync user periodically
|
||||
if setting.LdapEnabled {
|
||||
syncQuery := &m.LoginUserQuery{
|
||||
ReqContext: ctx,
|
||||
Username: proxyHeaderValue,
|
||||
}
|
||||
|
||||
if err := syncGrafanaUserWithLdapUser(syncQuery); err != nil {
|
||||
if err == login.ErrInvalidCredentials {
|
||||
ctx.Handle(500, "Unable to authenticate user", err)
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.Handle(500, "Failed to sync user", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
query.UserId = getRequestUserId(ctx)
|
||||
// if we're using ldap, pass authproxy login name to ldap user sync
|
||||
} else if setting.LdapEnabled {
|
||||
ctx.Session.Delete(session.SESS_KEY_LASTLDAPSYNC)
|
||||
|
||||
syncQuery := &m.LoginUserQuery{
|
||||
ReqContext: ctx,
|
||||
Username: proxyHeaderValue,
|
||||
}
|
||||
|
||||
if err := syncGrafanaUserWithLdapUser(syncQuery); err != nil {
|
||||
if err == login.ErrInvalidCredentials {
|
||||
ctx.Handle(500, "Unable to authenticate user", err)
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.Handle(500, "Failed to sync user", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if syncQuery.User == nil {
|
||||
ctx.Handle(500, "Failed to sync user", nil)
|
||||
return false
|
||||
}
|
||||
|
||||
query.UserId = syncQuery.User.Id
|
||||
// no ldap, just use the info we have
|
||||
} else {
|
||||
extUser := &m.ExternalUserInfo{
|
||||
AuthModule: "authproxy",
|
||||
AuthId: proxyHeaderValue,
|
||||
}
|
||||
|
||||
if setting.AuthProxyHeaderProperty == "username" {
|
||||
extUser.Login = proxyHeaderValue
|
||||
|
||||
// only set Email if it can be parsed as an email address
|
||||
emailAddr, emailErr := mail.ParseAddress(proxyHeaderValue)
|
||||
if emailErr == nil {
|
||||
extUser.Email = emailAddr.Address
|
||||
}
|
||||
} else if setting.AuthProxyHeaderProperty == "email" {
|
||||
extUser.Email = proxyHeaderValue
|
||||
extUser.Login = proxyHeaderValue
|
||||
} else {
|
||||
ctx.Handle(500, "Auth proxy header property invalid", nil)
|
||||
return true
|
||||
}
|
||||
|
||||
for _, field := range []string{"Name", "Email", "Login"} {
|
||||
if setting.AuthProxyHeaders[field] == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if val := ctx.Req.Header.Get(setting.AuthProxyHeaders[field]); val != "" {
|
||||
reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(val)
|
||||
}
|
||||
}
|
||||
|
||||
// add/update user in grafana
|
||||
cmd := &m.UpsertUserCommand{
|
||||
ReqContext: ctx,
|
||||
ExternalUser: extUser,
|
||||
SignupAllowed: setting.AuthProxyAutoSignUp,
|
||||
}
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
ctx.Handle(500, "Failed to login as user specified in auth proxy header", err)
|
||||
return true
|
||||
}
|
||||
|
||||
query.UserId = cmd.Result.Id
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
ctx.Handle(500, "Failed to find user", err)
|
||||
return true
|
||||
}
|
||||
|
||||
// Make sure that we cannot share a session between different users!
|
||||
if getRequestUserId(ctx) > 0 && getRequestUserId(ctx) != query.Result.UserId {
|
||||
// remove session
|
||||
@@ -77,16 +155,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// When ldap is enabled, sync userinfo and org roles
|
||||
if err := syncGrafanaUserWithLdapUser(ctx, query); err != nil {
|
||||
if err == login.ErrInvalidCredentials {
|
||||
ctx.Handle(500, "Unable to authenticate user", err)
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.Handle(500, "Failed to sync user", err)
|
||||
return false
|
||||
}
|
||||
ctx.Session.Set(AUTH_PROXY_SESSION_VAR, proxyHeaderValue)
|
||||
|
||||
ctx.SignedInUser = query.Result
|
||||
ctx.IsSignedIn = true
|
||||
@@ -95,81 +164,51 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
|
||||
if !setting.LdapEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
var syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
|
||||
expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix()
|
||||
|
||||
var lastLdapSync int64
|
||||
if lastLdapSyncInSession := ctx.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
|
||||
if lastLdapSyncInSession := query.ReqContext.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil {
|
||||
lastLdapSync = lastLdapSyncInSession.(int64)
|
||||
}
|
||||
|
||||
if lastLdapSync < expireEpoch {
|
||||
ldapCfg := login.LdapCfg
|
||||
|
||||
if len(ldapCfg.Servers) < 1 {
|
||||
return fmt.Errorf("No LDAP servers available")
|
||||
}
|
||||
|
||||
for _, server := range ldapCfg.Servers {
|
||||
author := login.NewLdapAuthenticator(server)
|
||||
if err := author.SyncSignedInUser(query.Result); err != nil {
|
||||
if err := author.SyncUser(query); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
|
||||
query.ReqContext.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAuthenticationProxy(ctx *m.ReqContext, proxyHeaderValue string) error {
|
||||
func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error {
|
||||
if len(strings.TrimSpace(setting.AuthProxyWhitelist)) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
proxies := strings.Split(setting.AuthProxyWhitelist, ",")
|
||||
remoteAddrSplit := strings.Split(ctx.Req.RemoteAddr, ":")
|
||||
sourceIP := remoteAddrSplit[0]
|
||||
|
||||
found := false
|
||||
for _, proxyIP := range proxies {
|
||||
if sourceIP == strings.TrimSpace(proxyIP) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
msg := fmt.Sprintf("Request for user (%s) is not from the authentication proxy", proxyHeaderValue)
|
||||
err := errors.New(msg)
|
||||
sourceIP, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSignedInUserQueryForProxyAuth(headerVal string) *m.GetSignedInUserQuery {
|
||||
query := m.GetSignedInUserQuery{}
|
||||
if setting.AuthProxyHeaderProperty == "username" {
|
||||
query.Login = headerVal
|
||||
} else if setting.AuthProxyHeaderProperty == "email" {
|
||||
query.Email = headerVal
|
||||
} else {
|
||||
panic("Auth proxy header property invalid")
|
||||
// Compare allowed IP addresses to actual address
|
||||
for _, proxyIP := range proxies {
|
||||
if sourceIP == strings.TrimSpace(proxyIP) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return &query
|
||||
}
|
||||
|
||||
func getCreateUserCommandForProxyAuth(headerVal string) *m.CreateUserCommand {
|
||||
cmd := m.CreateUserCommand{}
|
||||
if setting.AuthProxyHeaderProperty == "username" {
|
||||
cmd.Login = headerVal
|
||||
cmd.Email = headerVal
|
||||
} else if setting.AuthProxyHeaderProperty == "email" {
|
||||
cmd.Email = headerVal
|
||||
cmd.Login = headerVal
|
||||
} else {
|
||||
panic("Auth proxy header property invalid")
|
||||
}
|
||||
return &cmd
|
||||
return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP)
|
||||
}
|
||||
|
||||
@@ -26,57 +26,71 @@ func TestAuthProxyWithLdapEnabled(t *testing.T) {
|
||||
return &mockLdapAuther
|
||||
}
|
||||
|
||||
signedInUser := m.SignedInUser{}
|
||||
query := m.GetSignedInUserQuery{Result: &signedInUser}
|
||||
|
||||
Convey("When session variable lastLdapSync not set, call syncSignedInUser and set lastLdapSync", func() {
|
||||
Convey("When user logs in, call SyncUser", func() {
|
||||
// arrange
|
||||
sess := mockSession{}
|
||||
sess := newMockSession()
|
||||
ctx := m.ReqContext{Session: &sess}
|
||||
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeNil)
|
||||
|
||||
// act
|
||||
syncGrafanaUserWithLdapUser(&ctx, &query)
|
||||
syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
|
||||
ReqContext: &ctx,
|
||||
Username: "test",
|
||||
})
|
||||
|
||||
// assert
|
||||
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
|
||||
So(mockLdapAuther.syncUserCalled, ShouldBeTrue)
|
||||
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
|
||||
Convey("When session variable not expired, don't sync and don't change session var", func() {
|
||||
// arrange
|
||||
sess := mockSession{}
|
||||
sess := newMockSession()
|
||||
ctx := m.ReqContext{Session: &sess}
|
||||
now := time.Now().Unix()
|
||||
sess.Set(session.SESS_KEY_LASTLDAPSYNC, now)
|
||||
sess.Set(AUTH_PROXY_SESSION_VAR, "test")
|
||||
|
||||
// act
|
||||
syncGrafanaUserWithLdapUser(&ctx, &query)
|
||||
syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
|
||||
ReqContext: &ctx,
|
||||
Username: "test",
|
||||
})
|
||||
|
||||
// assert
|
||||
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldEqual, now)
|
||||
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeFalse)
|
||||
So(mockLdapAuther.syncUserCalled, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("When lastldapsync is expired, session variable should be updated", func() {
|
||||
// arrange
|
||||
sess := mockSession{}
|
||||
sess := newMockSession()
|
||||
ctx := m.ReqContext{Session: &sess}
|
||||
expiredTime := time.Now().Add(time.Duration(-120) * time.Minute).Unix()
|
||||
sess.Set(session.SESS_KEY_LASTLDAPSYNC, expiredTime)
|
||||
sess.Set(AUTH_PROXY_SESSION_VAR, "test")
|
||||
|
||||
// act
|
||||
syncGrafanaUserWithLdapUser(&ctx, &query)
|
||||
syncGrafanaUserWithLdapUser(&m.LoginUserQuery{
|
||||
ReqContext: &ctx,
|
||||
Username: "test",
|
||||
})
|
||||
|
||||
// assert
|
||||
So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, expiredTime)
|
||||
So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue)
|
||||
So(mockLdapAuther.syncUserCalled, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type mockSession struct {
|
||||
value interface{}
|
||||
value map[interface{}]interface{}
|
||||
}
|
||||
|
||||
func newMockSession() mockSession {
|
||||
session := mockSession{}
|
||||
session.value = make(map[interface{}]interface{})
|
||||
return session
|
||||
}
|
||||
|
||||
func (s *mockSession) Start(c *macaron.Context) error {
|
||||
@@ -84,15 +98,16 @@ func (s *mockSession) Start(c *macaron.Context) error {
|
||||
}
|
||||
|
||||
func (s *mockSession) Set(k interface{}, v interface{}) error {
|
||||
s.value = v
|
||||
s.value[k] = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockSession) Get(k interface{}) interface{} {
|
||||
return s.value
|
||||
return s.value[k]
|
||||
}
|
||||
|
||||
func (s *mockSession) Delete(k interface{}) interface{} {
|
||||
delete(s.value, k)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -113,21 +128,18 @@ func (s *mockSession) RegenerateId(c *macaron.Context) error {
|
||||
}
|
||||
|
||||
type mockLdapAuthenticator struct {
|
||||
syncSignedInUserCalled bool
|
||||
syncUserCalled bool
|
||||
}
|
||||
|
||||
func (a *mockLdapAuthenticator) Login(query *login.LoginUserQuery) error {
|
||||
func (a *mockLdapAuthenticator) Login(query *m.LoginUserQuery) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *mockLdapAuthenticator) SyncSignedInUser(signedInUser *m.SignedInUser) error {
|
||||
a.syncSignedInUserCalled = true
|
||||
func (a *mockLdapAuthenticator) SyncUser(query *m.LoginUserQuery) error {
|
||||
a.syncUserCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *mockLdapAuthenticator) GetGrafanaUserFor(ldapUser *login.LdapUserInfo) (*m.User, error) {
|
||||
func (a *mockLdapAuthenticator) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *login.LdapUserInfo) (*m.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (a *mockLdapAuthenticator) SyncOrgRoles(user *m.User, ldapUser *login.LdapUserInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/apikeygen"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
l "github.com/grafana/grafana/pkg/login"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -165,7 +164,7 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool {
|
||||
|
||||
user := loginQuery.Result
|
||||
|
||||
loginUserQuery := l.LoginUserQuery{Username: username, Password: password, User: user}
|
||||
loginUserQuery := m.LoginUserQuery{Username: username, Password: password, User: user}
|
||||
if err := bus.Dispatch(&loginUserQuery); err != nil {
|
||||
ctx.JsonApiErr(401, "Invalid username or password", err)
|
||||
return true
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
ms "github.com/go-macaron/session"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
l "github.com/grafana/grafana/pkg/login"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -72,7 +71,7 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(loginUserQuery *l.LoginUserQuery) error {
|
||||
bus.AddHandler("test", func(loginUserQuery *m.LoginUserQuery) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -177,12 +176,18 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
setting.AuthProxyEnabled = true
|
||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||
setting.AuthProxyHeaderProperty = "username"
|
||||
setting.LdapEnabled = false
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
||||
cmd.Result = &m.User{Id: 12}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.fakeReq("GET", "/")
|
||||
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
||||
sc.exec()
|
||||
@@ -199,18 +204,18 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||
setting.AuthProxyHeaderProperty = "username"
|
||||
setting.AuthProxyAutoSignUp = true
|
||||
setting.LdapEnabled = false
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
if query.UserId > 0 {
|
||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
|
||||
return nil
|
||||
} else {
|
||||
return m.ErrUserNotFound
|
||||
}
|
||||
return m.ErrUserNotFound
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.CreateUserCommand) error {
|
||||
cmd.Result = m.User{Id: 33}
|
||||
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
||||
cmd.Result = &m.User{Id: 33}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -226,11 +231,11 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
middlewareScenario("When auth_proxy is enabled and request RemoteAddr is not trusted", func(sc *scenarioContext) {
|
||||
middlewareScenario("When auth_proxy is enabled and IPv4 request RemoteAddr is not trusted", func(sc *scenarioContext) {
|
||||
setting.AuthProxyEnabled = true
|
||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||
setting.AuthProxyHeaderProperty = "username"
|
||||
setting.AuthProxyWhitelist = "192.168.1.1, 192.168.2.1"
|
||||
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
|
||||
|
||||
sc.fakeReq("GET", "/")
|
||||
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
||||
@@ -239,6 +244,24 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
|
||||
Convey("should return 407 status code", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 407)
|
||||
So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 192.168.3.1 is not from the authentication proxy")
|
||||
})
|
||||
})
|
||||
|
||||
middlewareScenario("When auth_proxy is enabled and IPv6 request RemoteAddr is not trusted", func(sc *scenarioContext) {
|
||||
setting.AuthProxyEnabled = true
|
||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||
setting.AuthProxyHeaderProperty = "username"
|
||||
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
|
||||
|
||||
sc.fakeReq("GET", "/")
|
||||
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
||||
sc.req.RemoteAddr = "[2001:23]:12345"
|
||||
sc.exec()
|
||||
|
||||
Convey("should return 407 status code", func() {
|
||||
So(sc.resp.Code, ShouldEqual, 407)
|
||||
So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 2001:23 is not from the authentication proxy")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -246,16 +269,21 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
setting.AuthProxyEnabled = true
|
||||
setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||
setting.AuthProxyHeaderProperty = "username"
|
||||
setting.AuthProxyWhitelist = "192.168.1.1, 192.168.2.1"
|
||||
setting.AuthProxyWhitelist = "192.168.1.1, 2001::23"
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
|
||||
cmd.Result = &m.User{Id: 33}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.fakeReq("GET", "/")
|
||||
sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
|
||||
sc.req.RemoteAddr = "192.168.2.1:12345"
|
||||
sc.req.RemoteAddr = "[2001::23]:12345"
|
||||
sc.exec()
|
||||
|
||||
Convey("Should init context with user info", func() {
|
||||
@@ -271,6 +299,11 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
setting.AuthProxyHeaderProperty = "username"
|
||||
setting.AuthProxyWhitelist = ""
|
||||
|
||||
bus.AddHandler("test", func(query *m.UpsertUserCommand) error {
|
||||
query.Result = &m.User{Id: 32}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
|
||||
return nil
|
||||
@@ -301,11 +334,17 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
setting.LdapEnabled = true
|
||||
|
||||
called := false
|
||||
syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error {
|
||||
syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
|
||||
called = true
|
||||
query.User = &m.User{Id: 32}
|
||||
return nil
|
||||
}
|
||||
|
||||
bus.AddHandler("test", func(query *m.UpsertUserCommand) error {
|
||||
query.Result = &m.User{Id: 32}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
|
||||
query.Result = &m.SignedInUser{OrgId: 4, UserId: 32}
|
||||
return nil
|
||||
|
||||
@@ -35,7 +35,7 @@ var (
|
||||
slash = []byte("/")
|
||||
)
|
||||
|
||||
// stack returns a nicely formated stack frame, skipping skip frames
|
||||
// stack returns a nicely formatted stack frame, skipping skip frames
|
||||
func stack(skip int) []byte {
|
||||
buf := new(bytes.Buffer) // the returned data
|
||||
// As we loop, we open files and read them. These variables record the currently
|
||||
|
||||
@@ -31,8 +31,6 @@ func initContextWithRenderAuth(ctx *m.ReqContext) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type renderContextFunc func(key string) (string, error)
|
||||
|
||||
func AddRenderAuthKey(orgId int64, userId int64, orgRole m.RoleType) string {
|
||||
renderKeysLock.Lock()
|
||||
|
||||
|
||||
+2
-2
@@ -34,8 +34,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCannotChangeStateOnPausedAlert error = fmt.Errorf("Cannot change state on pause alert")
|
||||
ErrRequiresNewState error = fmt.Errorf("update alert state requires a new state.")
|
||||
ErrCannotChangeStateOnPausedAlert = fmt.Errorf("Cannot change state on pause alert")
|
||||
ErrRequiresNewState = fmt.Errorf("update alert state requires a new state.")
|
||||
)
|
||||
|
||||
func (s AlertStateType) IsValid() bool {
|
||||
|
||||
@@ -56,7 +56,10 @@ type DashboardAclInfoDTO struct {
|
||||
UserId int64 `json:"userId"`
|
||||
UserLogin string `json:"userLogin"`
|
||||
UserEmail string `json:"userEmail"`
|
||||
UserAvatarUrl string `json:"userAvatarUrl"`
|
||||
TeamId int64 `json:"teamId"`
|
||||
TeamEmail string `json:"teamEmail"`
|
||||
TeamAvatarUrl string `json:"teamAvatarUrl"`
|
||||
Team string `json:"team"`
|
||||
Role *RoleType `json:"role,omitempty"`
|
||||
Permission PermissionType `json:"permission"`
|
||||
@@ -66,6 +69,7 @@ type DashboardAclInfoDTO struct {
|
||||
Slug string `json:"slug"`
|
||||
IsFolder bool `json:"isFolder"`
|
||||
Url string `json:"url"`
|
||||
Inherited bool `json:"inherited"`
|
||||
}
|
||||
|
||||
func (dto *DashboardAclInfoDTO) hasSameRoleAs(other *DashboardAclInfoDTO) bool {
|
||||
|
||||
+36
-24
@@ -13,26 +13,27 @@ import (
|
||||
|
||||
// Typed errors
|
||||
var (
|
||||
ErrDashboardNotFound = errors.New("Dashboard not found")
|
||||
ErrDashboardFolderNotFound = errors.New("Folder not found")
|
||||
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
|
||||
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
|
||||
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
|
||||
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
||||
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
|
||||
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
|
||||
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
|
||||
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
|
||||
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
|
||||
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
|
||||
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
|
||||
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
|
||||
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
|
||||
ErrDashboardFolderNameExists = errors.New("A folder with that name already exists")
|
||||
ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard")
|
||||
ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
|
||||
ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
|
||||
RootFolderName = "General"
|
||||
ErrDashboardNotFound = errors.New("Dashboard not found")
|
||||
ErrDashboardFolderNotFound = errors.New("Folder not found")
|
||||
ErrDashboardSnapshotNotFound = errors.New("Dashboard snapshot not found")
|
||||
ErrDashboardWithSameUIDExists = errors.New("A dashboard with the same uid already exists")
|
||||
ErrDashboardWithSameNameInFolderExists = errors.New("A dashboard with the same name in the folder already exists")
|
||||
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
||||
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
|
||||
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
|
||||
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
|
||||
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
|
||||
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
|
||||
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
|
||||
ErrDashboardTypeMismatch = errors.New("Dashboard cannot be changed to a folder")
|
||||
ErrDashboardFolderWithSameNameAsDashboard = errors.New("Folder name cannot be the same as one of its dashboards")
|
||||
ErrDashboardWithSameNameAsFolder = errors.New("Dashboard name cannot be the same as folder")
|
||||
ErrDashboardFolderNameExists = errors.New("A folder with that name already exists")
|
||||
ErrDashboardUpdateAccessDenied = errors.New("Access denied to save dashboard")
|
||||
ErrDashboardInvalidUid = errors.New("uid contains illegal characters")
|
||||
ErrDashboardUidToLong = errors.New("uid to long. max 40 characters")
|
||||
ErrDashboardCannotSaveProvisionedDashboard = errors.New("Cannot save provisioned dashboard")
|
||||
RootFolderName = "General"
|
||||
)
|
||||
|
||||
type UpdatePluginDashboardError struct {
|
||||
@@ -157,7 +158,7 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
|
||||
return dash
|
||||
}
|
||||
|
||||
// GetDashboardModel turns the command into the savable model
|
||||
// GetDashboardModel turns the command into the saveable model
|
||||
func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
|
||||
dash := NewDashboardFromJson(cmd.Dashboard)
|
||||
userId := cmd.UserId
|
||||
@@ -209,14 +210,14 @@ func GetDashboardFolderUrl(isFolder bool, uid string, slug string) string {
|
||||
return GetDashboardUrl(uid, slug)
|
||||
}
|
||||
|
||||
// Return the html url for a dashboard
|
||||
// GetDashboardUrl return the html url for a dashboard
|
||||
func GetDashboardUrl(uid string, slug string) string {
|
||||
return fmt.Sprintf("%s/d/%s/%s", setting.AppSubUrl, uid, slug)
|
||||
}
|
||||
|
||||
// Return the full url for a dashboard
|
||||
// GetFullDashboardUrl return the full url for a dashboard
|
||||
func GetFullDashboardUrl(uid string, slug string) string {
|
||||
return fmt.Sprintf("%s%s", setting.AppUrl, GetDashboardUrl(uid, slug))
|
||||
return fmt.Sprintf("%sd/%s/%s", setting.AppUrl, uid, slug)
|
||||
}
|
||||
|
||||
// GetFolderUrl return the html url for a folder
|
||||
@@ -224,6 +225,10 @@ func GetFolderUrl(folderUid string, slug string) string {
|
||||
return fmt.Sprintf("%s/dashboards/f/%s/%s", setting.AppSubUrl, folderUid, slug)
|
||||
}
|
||||
|
||||
type ValidateDashboardBeforeSaveResult struct {
|
||||
IsParentFolderChanged bool
|
||||
}
|
||||
|
||||
//
|
||||
// COMMANDS
|
||||
//
|
||||
@@ -268,6 +273,7 @@ type ValidateDashboardBeforeSaveCommand struct {
|
||||
OrgId int64
|
||||
Dashboard *Dashboard
|
||||
Overwrite bool
|
||||
Result *ValidateDashboardBeforeSaveResult
|
||||
}
|
||||
|
||||
//
|
||||
@@ -317,6 +323,12 @@ type GetDashboardSlugByIdQuery struct {
|
||||
Result string
|
||||
}
|
||||
|
||||
type IsDashboardProvisionedQuery struct {
|
||||
DashboardId int64
|
||||
|
||||
Result bool
|
||||
}
|
||||
|
||||
type GetProvisionedDashboardDataQuery struct {
|
||||
Name string
|
||||
|
||||
|
||||
@@ -4,11 +4,24 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDashboardModel(t *testing.T) {
|
||||
|
||||
Convey("Generate full dashboard url", t, func() {
|
||||
setting.AppUrl = "http://grafana.local/"
|
||||
fullUrl := GetFullDashboardUrl("uid", "my-dashboard")
|
||||
So(fullUrl, ShouldEqual, "http://grafana.local/d/uid/my-dashboard")
|
||||
})
|
||||
|
||||
Convey("Generate relative dashboard url", t, func() {
|
||||
setting.AppUrl = ""
|
||||
fullUrl := GetDashboardUrl("uid", "my-dashboard")
|
||||
So(fullUrl, ShouldEqual, "/d/uid/my-dashboard")
|
||||
})
|
||||
|
||||
Convey("When generating slug", t, func() {
|
||||
dashboard := NewDashboard("Grafana Play Home")
|
||||
dashboard.UpdateSlug()
|
||||
|
||||
@@ -58,7 +58,7 @@ type DataSource struct {
|
||||
Updated time.Time
|
||||
}
|
||||
|
||||
var knownDatasourcePlugins map[string]bool = map[string]bool{
|
||||
var knownDatasourcePlugins = map[string]bool{
|
||||
DS_ES: true,
|
||||
DS_GRAPHITE: true,
|
||||
DS_INFLUXDB: true,
|
||||
|
||||
@@ -33,7 +33,7 @@ func (ds *DataSource) GetHttpClient() (*http.Client, error) {
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Timeout: time.Duration(30 * time.Second),
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: transport,
|
||||
}, nil
|
||||
}
|
||||
|
||||
+10
-1
@@ -32,7 +32,7 @@ type Folder struct {
|
||||
HasAcl bool
|
||||
}
|
||||
|
||||
// GetDashboardModel turns the command into the savable model
|
||||
// GetDashboardModel turns the command into the saveable model
|
||||
func (cmd *CreateFolderCommand) GetDashboardModel(orgId int64, userId int64) *Dashboard {
|
||||
dashFolder := NewDashboardFolder(strings.TrimSpace(cmd.Title))
|
||||
dashFolder.OrgId = orgId
|
||||
@@ -89,3 +89,12 @@ type UpdateFolderCommand struct {
|
||||
|
||||
Result *Folder
|
||||
}
|
||||
|
||||
//
|
||||
// QUERIES
|
||||
//
|
||||
|
||||
type HasEditPermissionInFoldersQuery struct {
|
||||
SignedInUser *SignedInUser
|
||||
Result bool
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ type SendEmailCommandSync struct {
|
||||
}
|
||||
|
||||
type SendWebhookSync struct {
|
||||
Url string
|
||||
User string
|
||||
Password string
|
||||
Body string
|
||||
HttpMethod string
|
||||
HttpHeader map[string]string
|
||||
Url string
|
||||
User string
|
||||
Password string
|
||||
Body string
|
||||
HttpMethod string
|
||||
HttpHeader map[string]string
|
||||
ContentType string
|
||||
}
|
||||
|
||||
type SendResetPasswordEmailCommand struct {
|
||||
|
||||
@@ -48,9 +48,9 @@ func (r *RoleType) UnmarshalJSON(data []byte) error {
|
||||
|
||||
*r = RoleType(str)
|
||||
|
||||
if (*r).IsValid() == false {
|
||||
if !(*r).IsValid() {
|
||||
if (*r) != "" {
|
||||
return errors.New(fmt.Sprintf("JSON validation error: invalid role value: %s", *r))
|
||||
return fmt.Errorf("JSON validation error: invalid role value: %s", *r)
|
||||
}
|
||||
|
||||
*r = ROLE_VIEWER
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserAuth struct {
|
||||
Id int64
|
||||
UserId int64
|
||||
AuthModule string
|
||||
AuthId string
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
type ExternalUserInfo struct {
|
||||
AuthModule string
|
||||
AuthId string
|
||||
UserId int64
|
||||
Email string
|
||||
Login string
|
||||
Name string
|
||||
OrgRoles map[int64]RoleType
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// COMMANDS
|
||||
|
||||
type UpsertUserCommand struct {
|
||||
ReqContext *ReqContext
|
||||
ExternalUser *ExternalUserInfo
|
||||
SignupAllowed bool
|
||||
|
||||
Result *User
|
||||
}
|
||||
|
||||
type SetAuthInfoCommand struct {
|
||||
AuthModule string
|
||||
AuthId string
|
||||
UserId int64
|
||||
}
|
||||
|
||||
type DeleteAuthInfoCommand struct {
|
||||
UserAuth *UserAuth
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// QUERIES
|
||||
|
||||
type LoginUserQuery struct {
|
||||
ReqContext *ReqContext
|
||||
Username string
|
||||
Password string
|
||||
User *User
|
||||
IpAddress string
|
||||
}
|
||||
|
||||
type GetUserByAuthInfoQuery struct {
|
||||
AuthModule string
|
||||
AuthId string
|
||||
UserId int64
|
||||
Email string
|
||||
Login string
|
||||
|
||||
Result *User
|
||||
}
|
||||
|
||||
type GetAuthInfoQuery struct {
|
||||
AuthModule string
|
||||
AuthId string
|
||||
|
||||
Result *UserAuth
|
||||
}
|
||||
@@ -148,11 +148,11 @@ func (this *DashTemplateEvaluator) evalValue(source *simplejson.Json) interface{
|
||||
switch v := sourceValue.(type) {
|
||||
case string:
|
||||
interpolated := this.varRegex.ReplaceAllStringFunc(v, func(match string) string {
|
||||
if replacement, exists := this.variables[match]; exists {
|
||||
replacement, exists := this.variables[match]
|
||||
if exists {
|
||||
return replacement
|
||||
} else {
|
||||
return match
|
||||
}
|
||||
return match
|
||||
})
|
||||
return interpolated
|
||||
case bool:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
@@ -88,13 +87,14 @@ func TestDashboardImport(t *testing.T) {
|
||||
|
||||
func pluginScenario(desc string, t *testing.T, fn func()) {
|
||||
Convey("Given a plugin", t, func() {
|
||||
setting.Cfg = ini.Empty()
|
||||
sec, _ := setting.Cfg.NewSection("plugin.test-app")
|
||||
setting.Raw = ini.Empty()
|
||||
sec, _ := setting.Raw.NewSection("plugin.test-app")
|
||||
sec.NewKey("path", "../../tests/test-app")
|
||||
err := initPlugins(context.Background())
|
||||
|
||||
pm := &PluginManager{}
|
||||
err := pm.Init()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey(desc, fn)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@@ -15,10 +14,12 @@ import (
|
||||
func TestPluginDashboards(t *testing.T) {
|
||||
|
||||
Convey("When asking plugin dashboard info", t, func() {
|
||||
setting.Cfg = ini.Empty()
|
||||
sec, _ := setting.Cfg.NewSection("plugin.test-app")
|
||||
setting.Raw = ini.Empty()
|
||||
sec, _ := setting.Raw.NewSection("plugin.test-app")
|
||||
sec.NewKey("path", "../../tests/test-app")
|
||||
err := initPlugins(context.Background())
|
||||
|
||||
pm := &PluginManager{}
|
||||
err := pm.Init()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
@@ -11,10 +9,8 @@ func init() {
|
||||
bus.AddEventListener(handlePluginStateChanged)
|
||||
}
|
||||
|
||||
func updateAppDashboards() {
|
||||
time.Sleep(time.Second * 5)
|
||||
|
||||
plog.Debug("Looking for App Dashboard Updates")
|
||||
func (pm *PluginManager) updateAppDashboards() {
|
||||
pm.log.Debug("Looking for App Dashboard Updates")
|
||||
|
||||
query := m.GetPluginSettingsQuery{OrgId: 0}
|
||||
|
||||
@@ -38,23 +34,24 @@ func updateAppDashboards() {
|
||||
}
|
||||
|
||||
func autoUpdateAppDashboard(pluginDashInfo *PluginDashboardInfoDTO, orgId int64) error {
|
||||
if dash, err := loadPluginDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path); err != nil {
|
||||
dash, err := loadPluginDashboard(pluginDashInfo.PluginId, pluginDashInfo.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
plog.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev", pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision)
|
||||
updateCmd := ImportDashboardCommand{
|
||||
OrgId: orgId,
|
||||
PluginId: pluginDashInfo.PluginId,
|
||||
Overwrite: true,
|
||||
Dashboard: dash.Data,
|
||||
User: &m.SignedInUser{UserId: 0, OrgRole: m.ROLE_ADMIN},
|
||||
Path: pluginDashInfo.Path,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&updateCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
plog.Info("Auto updating App dashboard", "dashboard", dash.Title, "newRev", pluginDashInfo.Revision, "oldRev", pluginDashInfo.ImportedRevision)
|
||||
updateCmd := ImportDashboardCommand{
|
||||
OrgId: orgId,
|
||||
PluginId: pluginDashInfo.PluginId,
|
||||
Overwrite: true,
|
||||
Dashboard: dash.Data,
|
||||
User: &m.SignedInUser{UserId: 0, OrgRole: m.ROLE_ADMIN},
|
||||
Path: pluginDashInfo.Path,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&updateCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -122,15 +119,14 @@ func handlePluginStateChanged(event *m.PluginStateChangedEvent) error {
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
return err
|
||||
} else {
|
||||
for _, dash := range query.Result {
|
||||
deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id}
|
||||
}
|
||||
for _, dash := range query.Result {
|
||||
deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id}
|
||||
|
||||
plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
|
||||
plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
|
||||
|
||||
if err := bus.Dispatch(&deleteCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bus.Dispatch(&deleteCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestMappingRowValue(t *testing.T) {
|
||||
|
||||
boolRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BOOL, BoolValue: true})
|
||||
haveBool, ok := boolRowValue.(bool)
|
||||
if !ok || haveBool != true {
|
||||
if !ok || !haveBool {
|
||||
t.Fatalf("Expected true, was %v", haveBool)
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ func composeBinaryName(executable, os, arch string) string {
|
||||
return fmt.Sprintf("%s_%s_%s%s", executable, os, strings.ToLower(arch), extension)
|
||||
}
|
||||
|
||||
func (p *DataSourcePlugin) initBackendPlugin(ctx context.Context, log log.Logger) error {
|
||||
func (p *DataSourcePlugin) startBackendPlugin(ctx context.Context, log log.Logger) error {
|
||||
p.log = log.New("plugin-id", p.Id)
|
||||
|
||||
err := p.spawnSubProcess()
|
||||
|
||||
@@ -69,7 +69,7 @@ func (pb *PluginBase) registerPlugin(pluginDir string) error {
|
||||
|
||||
for _, include := range pb.Includes {
|
||||
if include.Role == "" {
|
||||
include.Role = m.RoleType(m.ROLE_VIEWER)
|
||||
include.Role = m.ROLE_VIEWER
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+51
-39
@@ -11,8 +11,10 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@@ -39,30 +41,12 @@ type PluginManager struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewPluginManager(ctx context.Context) (*PluginManager, error) {
|
||||
err := initPlugins(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PluginManager{
|
||||
log: log.New("plugins"),
|
||||
}, nil
|
||||
func init() {
|
||||
registry.RegisterService(&PluginManager{})
|
||||
}
|
||||
|
||||
func (p *PluginManager) Run(ctx context.Context) error {
|
||||
<-ctx.Done()
|
||||
|
||||
for _, p := range DataSources {
|
||||
p.Kill()
|
||||
}
|
||||
|
||||
p.log.Info("Stopped Plugins", "error", ctx.Err())
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func initPlugins(ctx context.Context) error {
|
||||
func (pm *PluginManager) Init() error {
|
||||
pm.log = log.New("plugins")
|
||||
plog = log.New("plugins")
|
||||
|
||||
DataSources = map[string]*DataSourcePlugin{}
|
||||
@@ -76,7 +60,7 @@ func initPlugins(ctx context.Context) error {
|
||||
"app": AppPlugin{},
|
||||
}
|
||||
|
||||
plog.Info("Starting plugin search")
|
||||
pm.log.Info("Starting plugin search")
|
||||
scan(path.Join(setting.StaticRootPath, "app/plugins"))
|
||||
|
||||
// check if plugins dir exists
|
||||
@@ -99,13 +83,6 @@ func initPlugins(ctx context.Context) error {
|
||||
}
|
||||
|
||||
for _, ds := range DataSources {
|
||||
if ds.Backend {
|
||||
err := ds.initBackendPlugin(ctx, plog)
|
||||
if err != nil {
|
||||
plog.Error("Failed to init plugin.", "error", err, "plugin", ds.Id)
|
||||
}
|
||||
}
|
||||
|
||||
ds.initFrontendPlugin()
|
||||
}
|
||||
|
||||
@@ -113,14 +90,49 @@ func initPlugins(ctx context.Context) error {
|
||||
app.initApp()
|
||||
}
|
||||
|
||||
go StartPluginUpdateChecker()
|
||||
go updateAppDashboards()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *PluginManager) startBackendPlugins(ctx context.Context) error {
|
||||
for _, ds := range DataSources {
|
||||
if ds.Backend {
|
||||
if err := ds.startBackendPlugin(ctx, plog); err != nil {
|
||||
pm.log.Error("Failed to init plugin.", "error", err, "plugin", ds.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pm *PluginManager) Run(ctx context.Context) error {
|
||||
pm.startBackendPlugins(ctx)
|
||||
pm.updateAppDashboards()
|
||||
pm.checkForUpdates()
|
||||
|
||||
ticker := time.NewTicker(time.Minute * 10)
|
||||
run := true
|
||||
|
||||
for run {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
pm.checkForUpdates()
|
||||
case <-ctx.Done():
|
||||
run = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// kil backend plugins
|
||||
for _, p := range DataSources {
|
||||
p.Kill()
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func checkPluginPaths() error {
|
||||
for _, section := range setting.Cfg.Sections() {
|
||||
for _, section := range setting.Raw.Sections() {
|
||||
if strings.HasPrefix(section.Name(), "plugin.") {
|
||||
path := section.Key("path").String()
|
||||
if path != "" {
|
||||
@@ -193,11 +205,11 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
|
||||
}
|
||||
|
||||
var loader PluginLoader
|
||||
if pluginGoType, exists := PluginTypes[pluginCommon.Type]; !exists {
|
||||
pluginGoType, exists := PluginTypes[pluginCommon.Type]
|
||||
if !exists {
|
||||
return errors.New("Unknown plugin type " + pluginCommon.Type)
|
||||
} else {
|
||||
loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader)
|
||||
}
|
||||
loader = reflect.New(reflect.TypeOf(pluginGoType)).Interface().(PluginLoader)
|
||||
|
||||
reader.Seek(0, 0)
|
||||
return loader.Load(jsonParser, currentDir)
|
||||
@@ -218,9 +230,9 @@ func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
|
||||
return make([]byte, 0), nil
|
||||
}
|
||||
|
||||
if data, err := ioutil.ReadFile(path); err != nil {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return data, nil
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -12,10 +11,12 @@ import (
|
||||
|
||||
func TestPluginScans(t *testing.T) {
|
||||
|
||||
Convey("When scaning for plugins", t, func() {
|
||||
Convey("When scanning for plugins", t, func() {
|
||||
setting.StaticRootPath, _ = filepath.Abs("../../public/")
|
||||
setting.Cfg = ini.Empty()
|
||||
err := initPlugins(context.Background())
|
||||
setting.Raw = ini.Empty()
|
||||
|
||||
pm := &PluginManager{}
|
||||
err := pm.Init()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(DataSources), ShouldBeGreaterThan, 1)
|
||||
@@ -27,10 +28,12 @@ func TestPluginScans(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("When reading app plugin definition", t, func() {
|
||||
setting.Cfg = ini.Empty()
|
||||
sec, _ := setting.Cfg.NewSection("plugin.nginx-app")
|
||||
setting.Raw = ini.Empty()
|
||||
sec, _ := setting.Raw.NewSection("plugin.nginx-app")
|
||||
sec.NewKey("path", "../../tests/test-app")
|
||||
err := initPlugins(context.Background())
|
||||
|
||||
pm := &PluginManager{}
|
||||
err := pm.Init()
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(Apps), ShouldBeGreaterThan, 0)
|
||||
|
||||
@@ -37,7 +37,7 @@ func GetPluginSettings(orgId int64) (map[string]*m.PluginSettingInfoDTO, error)
|
||||
|
||||
// if it's included in app check app settings
|
||||
if pluginDef.IncludedInAppId != "" {
|
||||
// app componets are by default disabled
|
||||
// app components are by default disabled
|
||||
opt.Enabled = false
|
||||
|
||||
if appSettings, ok := pluginMap[pluginDef.IncludedInAppId]; ok {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
httpClient http.Client = http.Client{Timeout: time.Duration(10 * time.Second)}
|
||||
httpClient = http.Client{Timeout: 10 * time.Second}
|
||||
)
|
||||
|
||||
type GrafanaNetPlugin struct {
|
||||
@@ -26,23 +26,6 @@ type GithubLatest struct {
|
||||
Testing string `json:"testing"`
|
||||
}
|
||||
|
||||
func StartPluginUpdateChecker() {
|
||||
if !setting.CheckForUpdates {
|
||||
return
|
||||
}
|
||||
|
||||
// do one check directly
|
||||
go checkForUpdates()
|
||||
|
||||
ticker := time.NewTicker(time.Minute * 10)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
checkForUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getAllExternalPluginSlugs() string {
|
||||
var result []string
|
||||
for _, plug := range Plugins {
|
||||
@@ -56,8 +39,12 @@ func getAllExternalPluginSlugs() string {
|
||||
return strings.Join(result, ",")
|
||||
}
|
||||
|
||||
func checkForUpdates() {
|
||||
log.Trace("Checking for updates")
|
||||
func (pm *PluginManager) checkForUpdates() {
|
||||
if !setting.CheckForUpdates {
|
||||
return
|
||||
}
|
||||
|
||||
pm.log.Debug("Checking for updates")
|
||||
|
||||
pluginSlugs := getAllExternalPluginSlugs()
|
||||
resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + pluginSlugs + "&grafanaVersion=" + setting.BuildVersion)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
var services = []Service{}
|
||||
|
||||
func RegisterService(srv Service) {
|
||||
services = append(services, srv)
|
||||
}
|
||||
|
||||
func GetServices() []Service {
|
||||
return services
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
Init() error
|
||||
}
|
||||
|
||||
// Useful for alerting service
|
||||
type CanBeDisabled interface {
|
||||
IsDisabled() bool
|
||||
}
|
||||
|
||||
type BackgroundService interface {
|
||||
Run(ctx context.Context) error
|
||||
}
|
||||
|
||||
func IsDisabled(srv Service) bool {
|
||||
canBeDisabled, ok := srv.(CanBeDisabled)
|
||||
return ok && canBeDisabled.IsDisabled()
|
||||
}
|
||||
@@ -13,11 +13,7 @@ func init() {
|
||||
func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
|
||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
||||
|
||||
if _, err := extractor.GetAlerts(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return extractor.ValidateAlerts()
|
||||
}
|
||||
|
||||
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
|
||||
@@ -29,15 +25,12 @@ func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
|
||||
|
||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
||||
|
||||
if alerts, err := extractor.GetAlerts(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
saveAlerts.Alerts = alerts
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&saveAlerts); err != nil {
|
||||
alerts, err := extractor.GetAlerts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
saveAlerts.Alerts = alerts
|
||||
|
||||
return bus.Dispatch(&saveAlerts)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
defaultTypes []string = []string{"gt", "lt"}
|
||||
rangedTypes []string = []string{"within_range", "outside_range"}
|
||||
defaultTypes = []string{"gt", "lt"}
|
||||
rangedTypes = []string{"within_range", "outside_range"}
|
||||
)
|
||||
|
||||
type AlertEvaluator interface {
|
||||
@@ -20,7 +20,7 @@ type AlertEvaluator interface {
|
||||
type NoValueEvaluator struct{}
|
||||
|
||||
func (e *NoValueEvaluator) Eval(reducedValue null.Float) bool {
|
||||
return reducedValue.Valid == false
|
||||
return !reducedValue.Valid
|
||||
}
|
||||
|
||||
type ThresholdEvaluator struct {
|
||||
@@ -45,7 +45,7 @@ func newThresholdEvaluator(typ string, model *simplejson.Json) (*ThresholdEvalua
|
||||
}
|
||||
|
||||
func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
|
||||
if reducedValue.Valid == false {
|
||||
if !reducedValue.Valid {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
|
||||
}
|
||||
|
||||
func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
|
||||
if reducedValue.Valid == false {
|
||||
if !reducedValue.Valid {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
|
||||
reducedValue := c.Reducer.Reduce(series)
|
||||
evalMatch := c.Evaluator.Eval(reducedValue)
|
||||
|
||||
if reducedValue.Valid == false {
|
||||
if !reducedValue.Valid {
|
||||
emptySerieCount++
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,14 @@ import (
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
execQueue chan *Job
|
||||
clock clock.Clock
|
||||
type AlertingService struct {
|
||||
execQueue chan *Job
|
||||
//clock clock.Clock
|
||||
ticker *Ticker
|
||||
scheduler Scheduler
|
||||
evalHandler EvalHandler
|
||||
@@ -25,35 +27,41 @@ type Engine struct {
|
||||
resultHandler ResultHandler
|
||||
}
|
||||
|
||||
func NewEngine() *Engine {
|
||||
e := &Engine{
|
||||
ticker: NewTicker(time.Now(), time.Second*0, clock.New()),
|
||||
execQueue: make(chan *Job, 1000),
|
||||
scheduler: NewScheduler(),
|
||||
evalHandler: NewEvalHandler(),
|
||||
ruleReader: NewRuleReader(),
|
||||
log: log.New("alerting.engine"),
|
||||
resultHandler: NewResultHandler(),
|
||||
}
|
||||
func init() {
|
||||
registry.RegisterService(&AlertingService{})
|
||||
}
|
||||
|
||||
func NewEngine() *AlertingService {
|
||||
e := &AlertingService{}
|
||||
e.Init()
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Engine) Run(ctx context.Context) error {
|
||||
e.log.Info("Initializing Alerting")
|
||||
func (e *AlertingService) IsDisabled() bool {
|
||||
return !setting.AlertingEnabled || !setting.ExecuteAlerts
|
||||
}
|
||||
|
||||
func (e *AlertingService) Init() error {
|
||||
e.ticker = NewTicker(time.Now(), time.Second*0, clock.New())
|
||||
e.execQueue = make(chan *Job, 1000)
|
||||
e.scheduler = NewScheduler()
|
||||
e.evalHandler = NewEvalHandler()
|
||||
e.ruleReader = NewRuleReader()
|
||||
e.log = log.New("alerting.engine")
|
||||
e.resultHandler = NewResultHandler()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *AlertingService) Run(ctx context.Context) error {
|
||||
alertGroup, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
alertGroup.Go(func() error { return e.alertingTicker(ctx) })
|
||||
alertGroup.Go(func() error { return e.runJobDispatcher(ctx) })
|
||||
|
||||
err := alertGroup.Wait()
|
||||
|
||||
e.log.Info("Stopped Alerting", "reason", err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
|
||||
func (e *AlertingService) alertingTicker(grafanaCtx context.Context) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Scheduler Panic: stopping alertingTicker", "error", err, "stack", log.Stack(1))
|
||||
@@ -78,7 +86,7 @@ func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
|
||||
func (e *AlertingService) runJobDispatcher(grafanaCtx context.Context) error {
|
||||
dispatcherGroup, alertCtx := errgroup.WithContext(grafanaCtx)
|
||||
|
||||
for {
|
||||
@@ -92,13 +100,13 @@ func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
|
||||
}
|
||||
|
||||
var (
|
||||
unfinishedWorkTimeout time.Duration = time.Second * 5
|
||||
unfinishedWorkTimeout = time.Second * 5
|
||||
// TODO: Make alertTimeout and alertMaxAttempts configurable in the config file.
|
||||
alertTimeout time.Duration = time.Second * 30
|
||||
alertMaxAttempts int = 3
|
||||
alertTimeout = time.Second * 30
|
||||
alertMaxAttempts = 3
|
||||
)
|
||||
|
||||
func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
|
||||
func (e *AlertingService) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
|
||||
@@ -133,7 +141,7 @@ func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
|
||||
func (e *AlertingService) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
|
||||
job.Running = false
|
||||
close(cancelChan)
|
||||
for cancelFn := range cancelChan {
|
||||
@@ -142,7 +150,7 @@ func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Engine) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
|
||||
func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type FakeEvalHandler struct {
|
||||
SuccessCallID int // 0 means never sucess
|
||||
SuccessCallID int // 0 means never success
|
||||
CallNb int
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestEngineProcessJob(t *testing.T) {
|
||||
|
||||
Convey("Should trigger as many retries as needed", func() {
|
||||
|
||||
Convey("never sucess -> max retries number", func() {
|
||||
Convey("never success -> max retries number", func() {
|
||||
expectedAttempts := alertMaxAttempts
|
||||
evalHandler := NewFakeEvalHandler(0)
|
||||
engine.evalHandler = evalHandler
|
||||
@@ -96,7 +96,7 @@ func TestEngineProcessJob(t *testing.T) {
|
||||
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
|
||||
})
|
||||
|
||||
Convey("always sucess -> never retry", func() {
|
||||
Convey("always success -> never retry", func() {
|
||||
expectedAttempts := 1
|
||||
evalHandler := NewFakeEvalHandler(1)
|
||||
engine.evalHandler = evalHandler
|
||||
@@ -105,7 +105,7 @@ func TestEngineProcessJob(t *testing.T) {
|
||||
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
|
||||
})
|
||||
|
||||
Convey("some errors before sucess -> some retries", func() {
|
||||
Convey("some errors before success -> some retries", func() {
|
||||
expectedAttempts := int(math.Ceil(float64(alertMaxAttempts) / 2))
|
||||
evalHandler := NewFakeEvalHandler(expectedAttempts)
|
||||
engine.evalHandler = evalHandler
|
||||
|
||||
@@ -106,11 +106,11 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
return setting.AppUrl, nil
|
||||
}
|
||||
|
||||
if ref, err := c.GetDashboardUID(); err != nil {
|
||||
ref, err := c.GetDashboardUID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
|
||||
}
|
||||
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
|
||||
}
|
||||
|
||||
func (c *EvalContext) GetNewState() m.AlertStateType {
|
||||
|
||||
@@ -11,76 +11,78 @@ import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
// DashAlertExtractor extracts alerts from the dashboard json
|
||||
type DashAlertExtractor struct {
|
||||
Dash *m.Dashboard
|
||||
OrgId int64
|
||||
OrgID int64
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewDashAlertExtractor(dash *m.Dashboard, orgId int64) *DashAlertExtractor {
|
||||
// NewDashAlertExtractor returns a new DashAlertExtractor
|
||||
func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor {
|
||||
return &DashAlertExtractor{
|
||||
Dash: dash,
|
||||
OrgId: orgId,
|
||||
OrgID: orgID,
|
||||
log: log.New("alerting.extractor"),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) lookupDatasourceId(dsName string) (*m.DataSource, error) {
|
||||
func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*m.DataSource, error) {
|
||||
if dsName == "" {
|
||||
query := &m.GetDataSourcesQuery{OrgId: e.OrgId}
|
||||
query := &m.GetDataSourcesQuery{OrgId: e.OrgID}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for _, ds := range query.Result {
|
||||
if ds.IsDefault {
|
||||
return ds, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, ds := range query.Result {
|
||||
if ds.IsDefault {
|
||||
return ds, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgId}
|
||||
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgID}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("Could not find datasource id for " + dsName)
|
||||
}
|
||||
|
||||
func findPanelQueryByRefId(panel *simplejson.Json, refId string) *simplejson.Json {
|
||||
func findPanelQueryByRefID(panel *simplejson.Json, refID string) *simplejson.Json {
|
||||
for _, targetsObj := range panel.Get("targets").MustArray() {
|
||||
target := simplejson.NewFromAny(targetsObj)
|
||||
|
||||
if target.Get("refId").MustString() == refId {
|
||||
if target.Get("refId").MustString() == refID {
|
||||
return target
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyJson(in *simplejson.Json) (*simplejson.Json, error) {
|
||||
rawJson, err := in.MarshalJSON()
|
||||
func copyJSON(in *simplejson.Json) (*simplejson.Json, error) {
|
||||
rawJSON, err := in.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return simplejson.NewJson(rawJson)
|
||||
return simplejson.NewJson(rawJSON)
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json) ([]*m.Alert, error) {
|
||||
func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, validateAlertFunc func(*m.Alert) bool) ([]*m.Alert, error) {
|
||||
alerts := make([]*m.Alert, 0)
|
||||
|
||||
for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
|
||||
panel := simplejson.NewFromAny(panelObj)
|
||||
|
||||
collapsedJson, collapsed := panel.CheckGet("collapsed")
|
||||
collapsedJSON, collapsed := panel.CheckGet("collapsed")
|
||||
// check if the panel is collapsed
|
||||
if collapsed && collapsedJson.MustBool() {
|
||||
if collapsed && collapsedJSON.MustBool() {
|
||||
|
||||
// extract alerts from sub panels for collapsed panels
|
||||
als, err := e.GetAlertFromPanels(panel)
|
||||
als, err := e.getAlertFromPanels(panel, validateAlertFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -95,14 +97,14 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
||||
continue
|
||||
}
|
||||
|
||||
panelId, err := panel.Get("id").Int64()
|
||||
panelID, err := panel.Get("id").Int64()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("panel id is required. err %v", err)
|
||||
}
|
||||
|
||||
// backward compatibility check, can be removed later
|
||||
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
|
||||
if hasEnabled && enabled.MustBool() == false {
|
||||
if hasEnabled && !enabled.MustBool() {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -113,8 +115,8 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
||||
|
||||
alert := &m.Alert{
|
||||
DashboardId: e.Dash.Id,
|
||||
OrgId: e.OrgId,
|
||||
PanelId: panelId,
|
||||
OrgId: e.OrgID,
|
||||
PanelId: panelID,
|
||||
Id: jsonAlert.Get("id").MustInt64(),
|
||||
Name: jsonAlert.Get("name").MustString(),
|
||||
Handler: jsonAlert.Get("handler").MustInt64(),
|
||||
@@ -126,11 +128,11 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
||||
jsonCondition := simplejson.NewFromAny(condition)
|
||||
|
||||
jsonQuery := jsonCondition.Get("query")
|
||||
queryRefId := jsonQuery.Get("params").MustArray()[0].(string)
|
||||
panelQuery := findPanelQueryByRefId(panel, queryRefId)
|
||||
queryRefID := jsonQuery.Get("params").MustArray()[0].(string)
|
||||
panelQuery := findPanelQueryByRefID(panel, queryRefID)
|
||||
|
||||
if panelQuery == nil {
|
||||
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
|
||||
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefID)
|
||||
return nil, ValidationError{Reason: reason}
|
||||
}
|
||||
|
||||
@@ -141,12 +143,13 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
||||
dsName = panel.Get("datasource").MustString()
|
||||
}
|
||||
|
||||
if datasource, err := e.lookupDatasourceId(dsName); err != nil {
|
||||
datasource, err := e.lookupDatasourceID(dsName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
|
||||
}
|
||||
|
||||
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
|
||||
|
||||
if interval, err := panel.Get("interval").String(); err == nil {
|
||||
panelQuery.Set("interval", interval)
|
||||
}
|
||||
@@ -162,21 +165,28 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if alert.ValidToSave() {
|
||||
alerts = append(alerts, alert)
|
||||
} else {
|
||||
if !validateAlertFunc(alert) {
|
||||
e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId)
|
||||
return nil, m.ErrDashboardContainsInvalidAlertData
|
||||
}
|
||||
|
||||
alerts = append(alerts, alert)
|
||||
}
|
||||
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
e.log.Debug("GetAlerts")
|
||||
func validateAlertRule(alert *m.Alert) bool {
|
||||
return alert.ValidToSave()
|
||||
}
|
||||
|
||||
dashboardJson, err := copyJson(e.Dash.Data)
|
||||
// GetAlerts extracts alerts from the dashboard json and does full validation on the alert json data
|
||||
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
return e.extractAlerts(validateAlertRule)
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) extractAlerts(validateFunc func(alert *m.Alert) bool) ([]*m.Alert, error) {
|
||||
dashboardJSON, err := copyJSON(e.Dash.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -185,11 +195,11 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
|
||||
// We extract alerts from rows to be backwards compatible
|
||||
// with the old dashboard json model.
|
||||
rows := dashboardJson.Get("rows").MustArray()
|
||||
rows := dashboardJSON.Get("rows").MustArray()
|
||||
if len(rows) > 0 {
|
||||
for _, rowObj := range rows {
|
||||
row := simplejson.NewFromAny(rowObj)
|
||||
a, err := e.GetAlertFromPanels(row)
|
||||
a, err := e.getAlertFromPanels(row, validateFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -197,7 +207,7 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
alerts = append(alerts, a...)
|
||||
}
|
||||
} else {
|
||||
a, err := e.GetAlertFromPanels(dashboardJson)
|
||||
a, err := e.getAlertFromPanels(dashboardJSON, validateFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -208,3 +218,10 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
// ValidateAlerts validates alerts in the dashboard json but does not require a valid dashboard id
|
||||
// in the first validation pass
|
||||
func (e *DashAlertExtractor) ValidateAlerts() error {
|
||||
_, err := e.extractAlerts(func(alert *m.Alert) bool { return alert.OrgId != 0 && alert.PanelId != 0 })
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -240,5 +240,26 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
So(len(alerts), ShouldEqual, 4)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Parse and validate dashboard without id and containing an alert", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/dash-without-id.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJSON, err := simplejson.NewJson(json)
|
||||
So(err, ShouldBeNil)
|
||||
dash := m.NewDashboardFromJson(dashJSON)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
|
||||
err = extractor.ValidateAlerts()
|
||||
|
||||
Convey("Should validate without error", func() {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should fail on save", func() {
|
||||
_, err := extractor.GetAlerts()
|
||||
So(err, ShouldEqual, m.ErrDashboardContainsInvalidAlertData)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,17 +87,17 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
IsAlertContext: true,
|
||||
}
|
||||
|
||||
if ref, err := context.GetDashboardUID(); err != nil {
|
||||
ref, err := context.GetDashboardUID()
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId)
|
||||
}
|
||||
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId)
|
||||
|
||||
if imagePath, err := renderer.RenderToPng(renderOpts); err != nil {
|
||||
imagePath, err := renderer.RenderToPng(renderOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
context.ImageOnDiskPath = imagePath
|
||||
}
|
||||
context.ImageOnDiskPath = imagePath
|
||||
|
||||
context.ImagePublicUrl, err = uploader.Upload(context.Ctx, context.ImageOnDiskPath)
|
||||
if err != nil {
|
||||
@@ -117,12 +117,12 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []
|
||||
|
||||
var result []Notifier
|
||||
for _, notification := range query.Result {
|
||||
if not, err := n.createNotifierFor(notification); err != nil {
|
||||
not, err := n.createNotifierFor(notification)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if not.ShouldNotify(context) {
|
||||
result = append(result, not)
|
||||
}
|
||||
}
|
||||
if not.ShouldNotify(context) {
|
||||
result = append(result, not)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Not
|
||||
|
||||
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
|
||||
|
||||
var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)
|
||||
var notifierFactories = make(map[string]*NotifierPlugin)
|
||||
|
||||
func RegisterNotifier(plugin *NotifierPlugin) {
|
||||
notifierFactories[plugin.Type] = plugin
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user