Merge branch 'master' into dashboard-acl-ux2

This commit is contained in:
Daniel Lee
2018-03-29 11:32:24 +02:00
586 changed files with 105630 additions and 8859 deletions

View File

@@ -5,34 +5,18 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
type UpdateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *m.Dashboard
}
type ValidateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *m.Dashboard
}
func init() {
bus.AddHandler("alerting", updateDashboardAlerts)
bus.AddHandler("alerting", validateDashboardAlerts)
}
func validateDashboardAlerts(cmd *ValidateDashboardAlertsCommand) error {
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 *UpdateDashboardAlertsCommand) error {
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
saveAlerts := m.SaveAlertsCommand{
OrgId: cmd.OrgId,
UserId: cmd.UserId,
@@ -41,15 +25,12 @@ func updateDashboardAlerts(cmd *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)
}

View File

@@ -86,17 +86,63 @@ func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
case <-grafanaCtx.Done():
return dispatcherGroup.Wait()
case job := <-e.execQueue:
dispatcherGroup.Go(func() error { return e.processJob(alertCtx, job) })
dispatcherGroup.Go(func() error { return e.processJobWithRetry(alertCtx, job) })
}
}
}
var (
unfinishedWorkTimeout time.Duration = time.Second * 5
alertTimeout time.Duration = time.Second * 30
// TODO: Make alertTimeout and alertMaxAttempts configurable in the config file.
alertTimeout time.Duration = time.Second * 30
alertMaxAttempts int = 3
)
func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
func (e *Engine) 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))
}
}()
cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
attemptChan := make(chan int, 1)
// Initialize with first attemptID=1
attemptChan <- 1
job.Running = true
for {
select {
case <-grafanaCtx.Done():
// In case grafana server context is cancel, let a chance to job processing
// to finish gracefully - by waiting a timeout duration - before forcing its end.
unfinishedWorkTimer := time.NewTimer(unfinishedWorkTimeout)
select {
case <-unfinishedWorkTimer.C:
return e.endJob(grafanaCtx.Err(), cancelChan, job)
case <-attemptChan:
return e.endJob(nil, cancelChan, job)
}
case attemptID, more := <-attemptChan:
if !more {
return e.endJob(nil, cancelChan, job)
}
go e.processJob(attemptID, attemptChan, cancelChan, job)
}
}
}
func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
job.Running = false
close(cancelChan)
for cancelFn := range cancelChan {
cancelFn()
}
return err
}
func (e *Engine) 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))
@@ -104,14 +150,13 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
}()
alertCtx, cancelFn := context.WithTimeout(context.Background(), alertTimeout)
cancelChan <- cancelFn
span := opentracing.StartSpan("alert execution")
alertCtx = opentracing.ContextWithSpan(alertCtx, span)
job.Running = true
evalContext := NewEvalContext(alertCtx, job.Rule)
evalContext.Ctx = alertCtx
done := make(chan struct{})
go func() {
defer func() {
if err := recover(); err != nil {
@@ -122,43 +167,36 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error {
tlog.String("message", "failed to execute alert rule. panic was recovered."),
)
span.Finish()
close(done)
close(attemptChan)
}
}()
e.evalHandler.Eval(evalContext)
e.resultHandler.Handle(evalContext)
span.SetTag("alertId", evalContext.Rule.Id)
span.SetTag("dashboardId", evalContext.Rule.DashboardId)
span.SetTag("firing", evalContext.Firing)
span.SetTag("nodatapoints", evalContext.NoDataFound)
span.SetTag("attemptID", attemptID)
if evalContext.Error != nil {
ext.Error.Set(span, true)
span.LogFields(
tlog.Error(evalContext.Error),
tlog.String("message", "alerting execution failed"),
tlog.String("message", "alerting execution attempt failed"),
)
if attemptID < alertMaxAttempts {
span.Finish()
e.log.Debug("Job Execution attempt triggered retry", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID)
attemptChan <- (attemptID + 1)
return
}
}
evalContext.Rule.State = evalContext.GetNewState()
e.resultHandler.Handle(evalContext)
span.Finish()
close(done)
e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID)
close(attemptChan)
}()
var err error = nil
select {
case <-grafanaCtx.Done():
select {
case <-time.After(unfinishedWorkTimeout):
cancelFn()
err = grafanaCtx.Err()
case <-done:
}
case <-done:
}
e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing)
job.Running = false
cancelFn()
return err
}

View File

@@ -0,0 +1,118 @@
package alerting
import (
"context"
"errors"
"math"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
type FakeEvalHandler struct {
SuccessCallID int // 0 means never sucess
CallNb int
}
func NewFakeEvalHandler(successCallID int) *FakeEvalHandler {
return &FakeEvalHandler{
SuccessCallID: successCallID,
CallNb: 0,
}
}
func (handler *FakeEvalHandler) Eval(evalContext *EvalContext) {
handler.CallNb++
if handler.CallNb != handler.SuccessCallID {
evalContext.Error = errors.New("Fake evaluation failure")
}
}
type FakeResultHandler struct{}
func (handler *FakeResultHandler) Handle(evalContext *EvalContext) error {
return nil
}
func TestEngineProcessJob(t *testing.T) {
Convey("Alerting engine job processing", t, func() {
engine := NewEngine()
engine.resultHandler = &FakeResultHandler{}
job := &Job{Running: true, Rule: &Rule{}}
Convey("Should trigger retry if needed", func() {
Convey("error + not last attempt -> retry", func() {
engine.evalHandler = NewFakeEvalHandler(0)
for i := 1; i < alertMaxAttempts; i++ {
attemptChan := make(chan int, 1)
cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
engine.processJob(i, attemptChan, cancelChan, job)
nextAttemptID, more := <-attemptChan
So(nextAttemptID, ShouldEqual, i+1)
So(more, ShouldEqual, true)
So(<-cancelChan, ShouldNotBeNil)
}
})
Convey("error + last attempt -> no retry", func() {
engine.evalHandler = NewFakeEvalHandler(0)
attemptChan := make(chan int, 1)
cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
engine.processJob(alertMaxAttempts, attemptChan, cancelChan, job)
nextAttemptID, more := <-attemptChan
So(nextAttemptID, ShouldEqual, 0)
So(more, ShouldEqual, false)
So(<-cancelChan, ShouldNotBeNil)
})
Convey("no error -> no retry", func() {
engine.evalHandler = NewFakeEvalHandler(1)
attemptChan := make(chan int, 1)
cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
engine.processJob(1, attemptChan, cancelChan, job)
nextAttemptID, more := <-attemptChan
So(nextAttemptID, ShouldEqual, 0)
So(more, ShouldEqual, false)
So(<-cancelChan, ShouldNotBeNil)
})
})
Convey("Should trigger as many retries as needed", func() {
Convey("never sucess -> max retries number", func() {
expectedAttempts := alertMaxAttempts
evalHandler := NewFakeEvalHandler(0)
engine.evalHandler = evalHandler
engine.processJobWithRetry(context.TODO(), job)
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
})
Convey("always sucess -> never retry", func() {
expectedAttempts := 1
evalHandler := NewFakeEvalHandler(1)
engine.evalHandler = evalHandler
engine.processJobWithRetry(context.TODO(), job)
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
})
Convey("some errors before sucess -> some retries", func() {
expectedAttempts := int(math.Ceil(float64(alertMaxAttempts) / 2))
evalHandler := NewFakeEvalHandler(expectedAttempts)
engine.evalHandler = evalHandler
engine.processJobWithRetry(context.TODO(), job)
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
})
})
})
}

View File

@@ -112,3 +112,34 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
}
}
func (c *EvalContext) GetNewState() m.AlertStateType {
if c.Error != nil {
c.log.Error("Alert Rule Result Error",
"ruleId", c.Rule.Id,
"name", c.Rule.Name,
"error", c.Error,
"changing state to", c.Rule.ExecutionErrorState.ToAlertState())
if c.Rule.ExecutionErrorState == m.ExecutionErrorKeepState {
return c.PrevAlertState
}
return c.Rule.ExecutionErrorState.ToAlertState()
} else if c.Firing {
return m.AlertStateAlerting
} else if c.NoDataFound {
c.log.Info("Alert Rule returned no data",
"ruleId", c.Rule.Id,
"name", c.Rule.Name,
"changing state to", c.Rule.NoDataState.ToAlertState())
if c.Rule.NoDataState == m.NoDataKeepState {
return c.PrevAlertState
}
return c.Rule.NoDataState.ToAlertState()
}
return m.AlertStateOK
}

View File

@@ -2,6 +2,7 @@ package alerting
import (
"context"
"fmt"
"testing"
"github.com/grafana/grafana/pkg/models"
@@ -12,7 +13,7 @@ func TestAlertingEvalContext(t *testing.T) {
Convey("Eval context", t, func() {
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
Convey("Should update alert state", func() {
Convey("Should update alert state when needed", func() {
Convey("ok -> alerting", func() {
ctx.PrevAlertState = models.AlertStateOK
@@ -28,5 +29,71 @@ func TestAlertingEvalContext(t *testing.T) {
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
})
})
Convey("Should compute and replace properly new rule state", func() {
dummieError := fmt.Errorf("dummie error")
Convey("ok -> alerting", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Firing = true
ctx.Rule.State = ctx.GetNewState()
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
})
Convey("ok -> error(alerting)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
ctx.Rule.State = ctx.GetNewState()
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
})
Convey("ok -> error(keep_last)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
ctx.Rule.State = ctx.GetNewState()
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
})
Convey("pending -> error(keep_last)", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
ctx.Rule.State = ctx.GetNewState()
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
})
Convey("ok -> no_data(alerting)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.NoDataState = models.NoDataSetAlerting
ctx.NoDataFound = true
ctx.Rule.State = ctx.GetNewState()
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
})
Convey("ok -> no_data(keep_last)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.NoDataState = models.NoDataKeepState
ctx.NoDataFound = true
ctx.Rule.State = ctx.GetNewState()
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
})
Convey("pending -> no_data(keep_last)", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Rule.NoDataState = models.NoDataKeepState
ctx.NoDataFound = true
ctx.Rule.State = ctx.GetNewState()
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
})
})
})
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/models"
)
type DefaultEvalHandler struct {
@@ -66,40 +65,7 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
context.Firing = firing
context.NoDataFound = noDataFound
context.EndTime = time.Now()
context.Rule.State = e.getNewState(context)
elapsedTime := context.EndTime.Sub(context.StartTime).Nanoseconds() / int64(time.Millisecond)
metrics.M_Alerting_Execution_Time.Observe(float64(elapsedTime))
}
// This should be move into evalContext once its been refactored. (Carl Bergquist)
func (handler *DefaultEvalHandler) getNewState(evalContext *EvalContext) models.AlertStateType {
if evalContext.Error != nil {
handler.log.Error("Alert Rule Result Error",
"ruleId", evalContext.Rule.Id,
"name", evalContext.Rule.Name,
"error", evalContext.Error,
"changing state to", evalContext.Rule.ExecutionErrorState.ToAlertState())
if evalContext.Rule.ExecutionErrorState == models.ExecutionErrorKeepState {
return evalContext.PrevAlertState
} else {
return evalContext.Rule.ExecutionErrorState.ToAlertState()
}
} else if evalContext.Firing {
return models.AlertStateAlerting
} else if evalContext.NoDataFound {
handler.log.Info("Alert Rule returned no data",
"ruleId", evalContext.Rule.Id,
"name", evalContext.Rule.Name,
"changing state to", evalContext.Rule.NoDataState.ToAlertState())
if evalContext.Rule.NoDataState == models.NoDataKeepState {
return evalContext.PrevAlertState
} else {
return evalContext.Rule.NoDataState.ToAlertState()
}
}
return models.AlertStateOK
}

View File

@@ -2,10 +2,8 @@ package alerting
import (
"context"
"fmt"
"testing"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
@@ -203,73 +201,5 @@ func TestAlertingEvaluationHandler(t *testing.T) {
handler.Eval(context)
So(context.NoDataFound, ShouldBeTrue)
})
Convey("EvalHandler can replace alert state based for errors and no_data", func() {
ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
dummieError := fmt.Errorf("dummie error")
Convey("Should update alert state", func() {
Convey("ok -> alerting", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Firing = true
So(handler.getNewState(ctx), ShouldEqual, models.AlertStateAlerting)
})
Convey("ok -> error(alerting)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
ctx.Rule.State = handler.getNewState(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
})
Convey("ok -> error(keep_last)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
ctx.Rule.State = handler.getNewState(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
})
Convey("pending -> error(keep_last)", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Error = dummieError
ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
ctx.Rule.State = handler.getNewState(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
})
Convey("ok -> no_data(alerting)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.NoDataState = models.NoDataSetAlerting
ctx.NoDataFound = true
ctx.Rule.State = handler.getNewState(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
})
Convey("ok -> no_data(keep_last)", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.NoDataState = models.NoDataKeepState
ctx.NoDataFound = true
ctx.Rule.State = handler.getNewState(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
})
Convey("pending -> no_data(keep_last)", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Rule.NoDataState = models.NoDataKeepState
ctx.NoDataFound = true
ctx.Rule.State = handler.getNewState(ctx)
So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
})
})
})
})
}

View File

@@ -11,76 +11,93 @@ 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")
// check if the panel is collapsed
if collapsed && collapsedJSON.MustBool() {
// extract alerts from sub panels for collapsed panels
als, err := e.getAlertFromPanels(panel, validateAlertFunc)
if err != nil {
return nil, err
}
alerts = append(alerts, als...)
continue
}
jsonAlert, hasAlert := panel.CheckGet("alert")
if !hasAlert {
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)
}
@@ -98,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(),
@@ -111,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}
}
@@ -126,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)
}
@@ -143,20 +161,32 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
// validate
_, err = NewRuleFromDBAlert(alert)
if err == nil && alert.ValidToSave() {
alerts = append(alerts, alert)
} else {
if err != nil {
return nil, err
}
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
}
@@ -165,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
}
@@ -177,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
}
@@ -188,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
}

View File

@@ -22,6 +22,7 @@ func TestAlertRuleExtraction(t *testing.T) {
defaultDs := &m.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
influxDBDs := &m.DataSource{Id: 16, OrgId: 1, Name: "InfluxDB"}
prom := &m.DataSource{Id: 17, OrgId: 1, Name: "Prometheus"}
bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error {
query.Result = []*m.DataSource{defaultDs, graphite2Ds}
@@ -38,6 +39,10 @@ func TestAlertRuleExtraction(t *testing.T) {
if query.Name == influxDBDs.Name {
query.Result = influxDBDs
}
if query.Name == prom.Name {
query.Result = prom
}
return nil
})
@@ -150,6 +155,22 @@ func TestAlertRuleExtraction(t *testing.T) {
})
})
Convey("Panel with id set to zero should return error", func() {
panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson([]byte(panelWithIdZero))
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1)
_, err = extractor.GetAlerts()
Convey("panel with id 0 should return error", func() {
So(err, ShouldNotBeNil)
})
})
Convey("Parse alerts from dashboard without rows", func() {
json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
So(err, ShouldBeNil)
@@ -198,5 +219,47 @@ func TestAlertRuleExtraction(t *testing.T) {
}
})
})
Convey("Should be able to extract collapsed panels", func() {
json, err := ioutil.ReadFile("./test-data/collapsed-panels.json")
So(err, ShouldBeNil)
dashJson, err := simplejson.NewJson(json)
So(err, ShouldBeNil)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewDashAlertExtractor(dash, 1)
alerts, err := extractor.GetAlerts()
Convey("Get rules without error", func() {
So(err, ShouldBeNil)
})
Convey("should be able to extract collapsed alerts", func() {
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)
})
})
})
}

View File

@@ -46,25 +46,49 @@ type AlertmanagerNotifier struct {
}
func (this *AlertmanagerNotifier) ShouldNotify(evalContext *alerting.EvalContext) bool {
this.log.Debug("Should notify", "ruleId", evalContext.Rule.Id, "state", evalContext.Rule.State, "previousState", evalContext.PrevAlertState)
// Do not notify when we become OK for the first time.
if (evalContext.PrevAlertState == m.AlertStatePending) && (evalContext.Rule.State == m.AlertStateOK) {
return false
}
// Notify on Alerting -> OK to resolve before alertmanager timeout.
if (evalContext.PrevAlertState == m.AlertStateAlerting) && (evalContext.Rule.State == m.AlertStateOK) {
return true
}
return evalContext.Rule.State == m.AlertStateAlerting
}
func (this *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error {
func (this *AlertmanagerNotifier) createAlert(evalContext *alerting.EvalContext, match *alerting.EvalMatch, ruleUrl string) *simplejson.Json {
alertJSON := simplejson.New()
alertJSON.Set("startsAt", evalContext.StartTime.UTC().Format(time.RFC3339))
if evalContext.Rule.State == m.AlertStateOK {
alertJSON.Set("endsAt", time.Now().UTC().Format(time.RFC3339))
}
alertJSON.Set("generatorURL", ruleUrl)
alerts := make([]interface{}, 0)
for _, match := range evalContext.EvalMatches {
alertJSON := simplejson.New()
alertJSON.Set("startsAt", evalContext.StartTime.UTC().Format(time.RFC3339))
if ruleUrl, err := evalContext.GetRuleUrl(); err == nil {
alertJSON.Set("generatorURL", ruleUrl)
// Annotations (summary and description are very commonly used).
alertJSON.SetPath([]string{"annotations", "summary"}, evalContext.Rule.Name)
description := ""
if evalContext.Rule.Message != "" {
description += evalContext.Rule.Message
}
if evalContext.Error != nil {
if description != "" {
description += "\n"
}
description += "Error: " + evalContext.Error.Error()
}
if description != "" {
alertJSON.SetPath([]string{"annotations", "description"}, description)
}
if evalContext.ImagePublicUrl != "" {
alertJSON.SetPath([]string{"annotations", "image"}, evalContext.ImagePublicUrl)
}
if evalContext.Rule.Message != "" {
alertJSON.SetPath([]string{"annotations", "description"}, evalContext.Rule.Message)
}
tags := make(map[string]string)
// Labels (from metrics tags + mandatory alertname).
tags := make(map[string]string)
if match != nil {
if len(match.Tags) == 0 {
tags["metric"] = match.Metric
} else {
@@ -72,10 +96,32 @@ func (this *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) erro
tags[k] = v
}
}
tags["alertname"] = evalContext.Rule.Name
alertJSON.Set("labels", tags)
}
tags["alertname"] = evalContext.Rule.Name
alertJSON.Set("labels", tags)
return alertJSON
}
alerts = append(alerts, alertJSON)
func (this *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending Alertmanager alert", "ruleId", evalContext.Rule.Id, "notification", this.Name)
ruleUrl, err := evalContext.GetRuleUrl()
if err != nil {
this.log.Error("Failed get rule link", "error", err)
return err
}
// Send one alert per matching series.
alerts := make([]interface{}, 0)
for _, match := range evalContext.EvalMatches {
alert := this.createAlert(evalContext, match, ruleUrl)
alerts = append(alerts, alert)
}
// This happens on ExecutionError or NoData
if len(alerts) == 0 {
alert := this.createAlert(evalContext, nil, ruleUrl)
alerts = append(alerts, alert)
}
bodyJSON := simplejson.NewFromAny(alerts)

View File

@@ -15,7 +15,11 @@ type NotifierBase struct {
}
func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase {
uploadImage := model.Get("uploadImage").MustBool(false)
uploadImage := true
value, exist := model.CheckGet("uploadImage")
if exist {
uploadImage = value.MustBool()
}
return NotifierBase{
Id: id,
@@ -27,15 +31,21 @@ func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model
}
func defaultShouldNotify(context *alerting.EvalContext) bool {
// Only notify on state change.
if context.PrevAlertState == context.Rule.State {
return false
}
// Do not notify when we become OK for the first time.
if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) {
return false
}
return true
}
func (n *NotifierBase) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (n *NotifierBase) GetType() string {
return n.Type
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
. "github.com/smartystreets/goconvey/convey"
@@ -11,6 +12,29 @@ import (
func TestBaseNotifier(t *testing.T) {
Convey("Base notifier tests", t, func() {
Convey("default constructor for notifiers", func() {
bJson := simplejson.New()
Convey("can parse false value", func() {
bJson.Set("uploadImage", false)
base := NewNotifierBase(1, false, "name", "email", bJson)
So(base.UploadImage, ShouldBeFalse)
})
Convey("can parse true value", func() {
bJson.Set("uploadImage", true)
base := NewNotifierBase(1, false, "name", "email", bJson)
So(base.UploadImage, ShouldBeTrue)
})
Convey("default value should be true for backwards compatibility", func() {
base := NewNotifierBase(1, false, "name", "email", bJson)
So(base.UploadImage, ShouldBeTrue)
})
})
Convey("should notify", func() {
Convey("pending -> ok", func() {
context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{

View File

@@ -38,10 +38,6 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}, nil
}
func (this *DingDingNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
type DingDingNotifier struct {
NotifierBase
Url string

View File

@@ -58,10 +58,6 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}, nil
}
func (this *EmailNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending alert notification to", "addresses", this.Addresses)

View File

@@ -75,10 +75,6 @@ type HipChatNotifier struct {
log log.Logger
}
func (this *HipChatNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -57,10 +57,6 @@ type KafkaNotifier struct {
log log.Logger
}
func (this *KafkaNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
state := evalContext.Rule.State

View File

@@ -51,10 +51,6 @@ type LineNotifier struct {
log log.Logger
}
func (this *LineNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -72,10 +72,6 @@ type OpsGenieNotifier struct {
log log.Logger
}
func (this *OpsGenieNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error {
var err error
@@ -99,11 +95,16 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err
return err
}
customData := "Triggered metrics:\n\n"
for _, evt := range evalContext.EvalMatches {
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
}
bodyJSON := simplejson.New()
bodyJSON.Set("message", evalContext.Rule.Name)
bodyJSON.Set("source", "Grafana")
bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
bodyJSON.Set("description", fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message))
bodyJSON.Set("description", fmt.Sprintf("%s - %s\n%s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message, customData))
details := simplejson.New()
details.Set("url", ruleUrl)

View File

@@ -1,7 +1,9 @@
package notifiers
import (
"os"
"strconv"
"time"
"fmt"
@@ -38,7 +40,7 @@ func init() {
}
var (
pagerdutyEventApiUrl string = "https://events.pagerduty.com/generic/2010-04-15/create_event.json"
pagerdutyEventApiUrl string = "https://events.pagerduty.com/v2/enqueue"
)
func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
@@ -63,10 +65,6 @@ type PagerdutyNotifier struct {
log log.Logger
}
func (this *PagerdutyNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
if evalContext.Rule.State == m.AlertStateOK && !this.AutoResolve {
@@ -85,28 +83,41 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Notifying Pagerduty", "event_type", eventType)
payloadJSON := simplejson.New()
payloadJSON.Set("summary", evalContext.Rule.Name+" - "+evalContext.Rule.Message)
if hostname, err := os.Hostname(); err == nil {
payloadJSON.Set("source", hostname)
}
payloadJSON.Set("severity", "critical")
payloadJSON.Set("timestamp", time.Now())
payloadJSON.Set("component", "Grafana")
payloadJSON.Set("custom_details", customData)
bodyJSON := simplejson.New()
bodyJSON.Set("service_key", this.Key)
bodyJSON.Set("description", evalContext.Rule.Name+" - "+evalContext.Rule.Message)
bodyJSON.Set("client", "Grafana")
bodyJSON.Set("details", customData)
bodyJSON.Set("event_type", eventType)
bodyJSON.Set("incident_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
bodyJSON.Set("routing_key", this.Key)
bodyJSON.Set("event_action", eventType)
bodyJSON.Set("dedup_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
bodyJSON.Set("payload", payloadJSON)
ruleUrl, err := evalContext.GetRuleUrl()
if err != nil {
this.log.Error("Failed get rule link", "error", err)
return err
}
links := make([]interface{}, 1)
linkJSON := simplejson.New()
linkJSON.Set("href", ruleUrl)
bodyJSON.Set("client_url", ruleUrl)
bodyJSON.Set("client", "Grafana")
links[0] = linkJSON
bodyJSON.Set("links", links)
if evalContext.ImagePublicUrl != "" {
contexts := make([]interface{}, 1)
imageJSON := simplejson.New()
imageJSON.Set("type", "image")
imageJSON.Set("src", evalContext.ImagePublicUrl)
contexts[0] = imageJSON
bodyJSON.Set("contexts", contexts)
bodyJSON.Set("images", contexts)
}
body, _ := bodyJSON.MarshalJSON()
@@ -115,6 +126,9 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
Url: pagerdutyEventApiUrl,
Body: string(body),
HttpMethod: "POST",
HttpHeader: map[string]string{
"Content-Type": "application/json",
},
}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {

View File

@@ -123,10 +123,6 @@ type PushoverNotifier struct {
log log.Logger
}
func (this *PushoverNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
ruleUrl, err := evalContext.GetRuleUrl()
if err != nil {

View File

@@ -71,10 +71,6 @@ type SensuNotifier struct {
log log.Logger
}
func (this *SensuNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending sensu result")

View File

@@ -98,10 +98,6 @@ type SlackNotifier struct {
log log.Logger
}
func (this *SlackNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -47,10 +47,6 @@ type TeamsNotifier struct {
log log.Logger
}
func (this *TeamsNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing teams notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)
@@ -82,6 +78,8 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
message := this.Mention
if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
message += " " + evalContext.Rule.Message
} else {
message += " " // summary must not be empty
}
body := map[string]interface{}{

View File

@@ -12,6 +12,10 @@ import (
"os"
)
const (
captionLengthLimit = 200
)
var (
telegramApiUrl string = "https://api.telegram.org/bot%s/%s"
)
@@ -82,88 +86,81 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}
func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) *m.SendWebhookSync {
var imageFile *os.File
var err error
if sendImageInline {
imageFile, err = os.Open(evalContext.ImageOnDiskPath)
defer imageFile.Close()
if err != nil {
sendImageInline = false // fall back to text message
cmd, err := this.buildMessageInlineImage(evalContext)
if err == nil {
return cmd
} else {
this.log.Error("Could not generate Telegram message with inline image.", "err", err)
}
}
message := ""
return this.buildMessageLinkedImage(evalContext)
}
if sendImageInline {
// Telegram's API does not allow HTML formatting for image captions.
message = fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
} else {
message = fmt.Sprintf("<b>%s</b>\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
}
func (this *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalContext) *m.SendWebhookSync {
message := fmt.Sprintf("<b>%s</b>\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
ruleUrl, err := evalContext.GetRuleUrl()
if err == nil {
message = message + fmt.Sprintf("URL: %s\n", ruleUrl)
}
if !sendImageInline {
// only attach this if we are not sending it inline.
if evalContext.ImagePublicUrl != "" {
message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl)
}
}
metrics := ""
fieldLimitCount := 4
for index, evt := range evalContext.EvalMatches {
metrics += fmt.Sprintf("\n%s: %s", evt.Metric, evt.Value)
if index > fieldLimitCount {
break
}
if evalContext.ImagePublicUrl != "" {
message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl)
}
metrics := generateMetricsMessage(evalContext)
if metrics != "" {
if sendImageInline {
// Telegram's API does not allow HTML formatting for image captions.
message = message + fmt.Sprintf("\nMetrics:%s", metrics)
} else {
message = message + fmt.Sprintf("\n<i>Metrics:</i>%s", metrics)
}
message = message + fmt.Sprintf("\n<i>Metrics:</i>%s", metrics)
}
var body bytes.Buffer
cmd := this.generateTelegramCmd(message, "text", "sendMessage", func(w *multipart.Writer) {
fw, _ := w.CreateFormField("parse_mode")
fw.Write([]byte("html"))
})
return cmd
}
func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.EvalContext) (*m.SendWebhookSync, error) {
var imageFile *os.File
var err error
imageFile, err = os.Open(evalContext.ImageOnDiskPath)
defer imageFile.Close()
if err != nil {
return nil, err
}
ruleUrl, err := evalContext.GetRuleUrl()
metrics := generateMetricsMessage(evalContext)
message := generateImageCaption(evalContext, ruleUrl, metrics)
cmd := this.generateTelegramCmd(message, "caption", "sendPhoto", func(w *multipart.Writer) {
fw, _ := w.CreateFormFile("photo", evalContext.ImageOnDiskPath)
io.Copy(fw, imageFile)
})
return cmd, nil
}
func (this *TelegramNotifier) generateTelegramCmd(message string, messageField string, apiAction string, extraConf func(writer *multipart.Writer)) *m.SendWebhookSync {
var body bytes.Buffer
w := multipart.NewWriter(&body)
fw, _ := w.CreateFormField("chat_id")
fw.Write([]byte(this.ChatID))
if sendImageInline {
fw, _ = w.CreateFormField("caption")
fw.Write([]byte(message))
fw, _ = w.CreateFormField(messageField)
fw.Write([]byte(message))
fw, _ = w.CreateFormFile("photo", evalContext.ImageOnDiskPath)
io.Copy(fw, imageFile)
} else {
fw, _ = w.CreateFormField("text")
fw.Write([]byte(message))
fw, _ = w.CreateFormField("parse_mode")
fw.Write([]byte("html"))
}
extraConf(w)
w.Close()
apiMethod := ""
if sendImageInline {
this.log.Info("Sending telegram image notification", "photo", evalContext.ImageOnDiskPath, "chat_id", this.ChatID, "bot_token", this.BotToken)
apiMethod = "sendPhoto"
} else {
this.log.Info("Sending telegram text notification", "chat_id", this.ChatID, "bot_token", this.BotToken)
apiMethod = "sendMessage"
}
this.log.Info("Sending telegram notification", "chat_id", this.ChatID, "bot_token", this.BotToken, "apiAction", apiAction)
url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiAction)
url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiMethod)
cmd := &m.SendWebhookSync{
Url: url,
Body: body.String(),
@@ -175,8 +172,49 @@ func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, se
return cmd
}
func (this *TelegramNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
func generateMetricsMessage(evalContext *alerting.EvalContext) string {
metrics := ""
fieldLimitCount := 4
for index, evt := range evalContext.EvalMatches {
metrics += fmt.Sprintf("\n%s: %s", evt.Metric, evt.Value)
if index > fieldLimitCount {
break
}
}
return metrics
}
func generateImageCaption(evalContext *alerting.EvalContext, ruleUrl string, metrics string) string {
message := evalContext.GetNotificationTitle()
if len(evalContext.Rule.Message) > 0 {
message = fmt.Sprintf("%s\nMessage: %s", message, evalContext.Rule.Message)
}
if len(message) > captionLengthLimit {
message = message[0:captionLengthLimit]
}
if len(ruleUrl) > 0 {
urlLine := fmt.Sprintf("\nURL: %s", ruleUrl)
message = appendIfPossible(message, urlLine, captionLengthLimit)
}
if metrics != "" {
metricsLines := fmt.Sprintf("\n\nMetrics:%s", metrics)
message = appendIfPossible(message, metricsLines, captionLengthLimit)
}
return message
}
func appendIfPossible(message string, extra string, sizeLimit int) string {
if len(extra)+len(message) <= sizeLimit {
return message + extra
}
log.Debug("Line too long for image caption.", "value", extra)
return message
}
func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {

View File

@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
. "github.com/smartystreets/goconvey/convey"
)
@@ -50,6 +51,71 @@ func TestTelegramNotifier(t *testing.T) {
So(telegramNotifier.ChatID, ShouldEqual, "-1234567890")
})
Convey("generateCaption should generate a message with all pertinent details", func() {
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message.",
State: m.AlertStateOK,
})
caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "")
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
So(caption, ShouldContainSubstring, "Some kind of message.")
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
So(caption, ShouldContainSubstring, "http://grafa.url/abcdef")
})
Convey("When generating a message", func() {
Convey("URL should be skipped if it's too long", func() {
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message.",
State: m.AlertStateOK,
})
caption := generateImageCaption(evalContext,
"http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"foo bar")
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
So(caption, ShouldContainSubstring, "Some kind of message.")
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
So(caption, ShouldContainSubstring, "foo bar")
So(caption, ShouldNotContainSubstring, "http")
})
Convey("Message should be trimmed if it's too long", func() {
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it.",
State: m.AlertStateOK,
})
caption := generateImageCaption(evalContext,
"http://grafa.url/foo",
"")
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
So(caption, ShouldNotContainSubstring, "http")
So(caption, ShouldContainSubstring, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise ")
})
Convey("Metrics should be skipped if they dont fit", func() {
evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I ",
State: m.AlertStateOK,
})
caption := generateImageCaption(evalContext,
"http://grafa.url/foo",
"foo bar long song")
So(len(caption), ShouldBeLessThanOrEqualTo, 200)
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
So(caption, ShouldNotContainSubstring, "http")
So(caption, ShouldNotContainSubstring, "foo bar")
})
})
})
})
}

View File

@@ -114,10 +114,6 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}, nil
}
func (this *ThreemaNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error {
notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID)
notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID)

View File

@@ -68,10 +68,6 @@ type VictoropsNotifier struct {
log log.Logger
}
func (this *VictoropsNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
// Notify sends notification to Victorops via POST to URL endpoint
func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing victorops notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -65,10 +65,6 @@ type WebhookNotifier struct {
log log.Logger
}
func (this *WebhookNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending webhook")

View File

@@ -0,0 +1,597 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 127,
"links": [],
"panels": [
{
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 9,
"title": "Row title",
"type": "row"
},
{
"alert": {
"conditions": [
{
"evaluator": {
"params": [
200
],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"A",
"5m",
"now"
]
},
"reducer": {
"params": [],
"type": "avg"
},
"type": "query"
}
],
"executionErrorState": "alerting",
"frequency": "10s",
"handler": 1,
"name": "Panel Title alert",
"noDataState": "no_data",
"notifications": []
},
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 1
},
"id": 10,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "{{job}}",
"refId": "A"
}
],
"thresholds": [
{
"colorMode": "critical",
"fill": true,
"line": true,
"op": "gt",
"value": 200
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 1
},
"id": 14,
"limit": 10,
"links": [],
"onlyAlertsOnDashboard": true,
"show": "current",
"sortOrder": 1,
"stateFilter": [],
"title": "Panel Title",
"type": "alertlist"
},
{
"collapsed": true,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 10
},
"id": 6,
"panels": [
{
"alert": {
"conditions": [
{
"evaluator": {
"params": [
200
],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"A",
"5m",
"now"
]
},
"reducer": {
"params": [],
"type": "avg"
},
"type": "query"
}
],
"executionErrorState": "alerting",
"frequency": "10s",
"handler": 1,
"name": "Panel 2 alert",
"noDataState": "no_data",
"notifications": []
},
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 11
},
"id": 11,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "{{job}}",
"refId": "A"
}
],
"thresholds": [
{
"colorMode": "critical",
"fill": true,
"line": true,
"op": "gt",
"value": 200
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel 2",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"alert": {
"conditions": [
{
"evaluator": {
"params": [
200
],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"A",
"5m",
"now"
]
},
"reducer": {
"params": [],
"type": "avg"
},
"type": "query"
}
],
"executionErrorState": "alerting",
"frequency": "10s",
"handler": 1,
"name": "Panel 4 alert",
"noDataState": "no_data",
"notifications": []
},
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 11
},
"id": 15,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "{{job}}",
"refId": "A"
}
],
"thresholds": [
{
"colorMode": "critical",
"fill": true,
"line": true,
"op": "gt",
"value": 200
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel 4",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"title": "Row title",
"type": "row"
},
{
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 11
},
"id": 4,
"title": "Row title",
"type": "row"
},
{
"alert": {
"conditions": [
{
"evaluator": {
"params": [
200
],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"A",
"5m",
"now"
]
},
"reducer": {
"params": [],
"type": "avg"
},
"type": "query"
}
],
"executionErrorState": "alerting",
"frequency": "10s",
"handler": 1,
"name": "Panel 3 alert",
"noDataState": "no_data",
"notifications": []
},
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fill": 1,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 12
},
"id": 12,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"legendFormat": "{{job}}",
"refId": "A"
}
],
"thresholds": [
{
"colorMode": "critical",
"fill": true,
"line": true,
"op": "gt",
"value": 200
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel 3",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
}
],
"schemaVersion": 16,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "New dashboard Copy",
"uid": "6v5pg36zk",
"version": 17
}

View File

@@ -0,0 +1,281 @@
{
"title": "Influxdb",
"tags": [
"apa"
],
"style": "dark",
"timezone": "browser",
"editable": true,
"hideControls": false,
"sharedCrosshair": false,
"rows": [
{
"collapse": false,
"editable": true,
"height": "450px",
"panels": [
{
"alert": {
"conditions": [
{
"evaluator": {
"params": [
10
],
"type": "gt"
},
"query": {
"params": [
"B",
"5m",
"now"
]
},
"reducer": {
"params": [],
"type": "avg"
},
"type": "query"
}
],
"frequency": "3s",
"handler": 1,
"name": "Influxdb",
"noDataState": "no_data",
"notifications": [
{
"id": 6
}
]
},
"alerting": {},
"aliasColors": {
"logins.count.count": "#890F02"
},
"bars": false,
"datasource": "InfluxDB",
"editable": true,
"error": false,
"fill": 1,
"grid": {},
"id": 1,
"interval": ">10s",
"isNew": true,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"groupBy": [
{
"params": [
"$interval"
],
"type": "time"
},
{
"params": [
"datacenter"
],
"type": "tag"
},
{
"params": [
"none"
],
"type": "fill"
}
],
"hide": false,
"measurement": "logins.count",
"policy": "default",
"query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
"rawQuery": true,
"refId": "B",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "count"
}
]
],
"tags": []
},
{
"groupBy": [
{
"params": [
"$interval"
],
"type": "time"
},
{
"params": [
"null"
],
"type": "fill"
}
],
"hide": true,
"measurement": "cpu",
"policy": "default",
"refId": "A",
"resultFormat": "time_series",
"select": [
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "mean"
}
],
[
{
"params": [
"value"
],
"type": "field"
},
{
"params": [],
"type": "sum"
}
]
],
"tags": []
}
],
"thresholds": [
{
"colorMode": "critical",
"fill": true,
"line": true,
"op": "gt",
"value": 10
}
],
"timeFrom": null,
"timeShift": null,
"title": "Panel Title",
"tooltip": {
"msResolution": false,
"ordering": "alphabetical",
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"editable": true,
"error": false,
"id": 2,
"isNew": true,
"limit": 10,
"links": [],
"show": "current",
"span": 2,
"stateFilter": [
"alerting"
],
"title": "Alert status",
"type": "alertlist"
}
],
"title": "Row"
}
],
"time": {
"from": "now-5m",
"to": "now"
},
"timepicker": {
"now": true,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"schemaVersion": 13,
"version": 120,
"links": [],
"gnetId": null
}

View File

@@ -279,4 +279,4 @@
"version": 120,
"links": [],
"gnetId": null
}
}

View File

@@ -0,0 +1,63 @@
{
"id": 57,
"title": "Graphite 4",
"originalTitle": "Graphite 4",
"tags": ["graphite"],
"rows": [
{
"panels": [
{
"title": "Active desktop users",
"id": 0,
"editable": true,
"type": "graph",
"targets": [
{
"refId": "A",
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
}
],
"datasource": null,
"alert": {
"name": "name1",
"message": "desc1",
"handler": 1,
"frequency": "60s",
"conditions": [
{
"type": "query",
"query": {"params": ["A", "5m", "now"]},
"reducer": {"type": "avg", "params": []},
"evaluator": {"type": ">", "params": [100]}
}
]
}
},
{
"title": "Active mobile users",
"id": 4,
"targets": [
{"refId": "A", "target": ""},
{"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
],
"datasource": "graphite2",
"alert": {
"name": "name2",
"message": "desc2",
"handler": 0,
"frequency": "60s",
"severity": "warning",
"conditions": [
{
"type": "query",
"query": {"params": ["B", "5m", "now"]},
"reducer": {"type": "avg", "params": []},
"evaluator": {"type": ">", "params": [100]}
}
]
}
}
]
}
]
}

View File

@@ -53,6 +53,7 @@ func testAlertRule(rule *Rule) *EvalContext {
context.IsTestRun = true
handler.Eval(context)
context.Rule.State = context.GetNewState()
return context
}

View File

@@ -83,11 +83,21 @@ func (service *CleanUpService) cleanUpTmpFiles() {
}
func (service *CleanUpService) deleteExpiredSnapshots() {
bus.Dispatch(&m.DeleteExpiredSnapshotsCommand{})
cmd := m.DeleteExpiredSnapshotsCommand{}
if err := bus.Dispatch(&cmd); err != nil {
service.log.Error("Failed to delete expired snapshots", "error", err.Error())
} else {
service.log.Debug("Deleted expired snapshots", "rows affected", cmd.DeletedRows)
}
}
func (service *CleanUpService) deleteExpiredDashboardVersions() {
bus.Dispatch(&m.DeleteExpiredVersionsCommand{})
cmd := m.DeleteExpiredVersionsCommand{}
if err := bus.Dispatch(&cmd); err != nil {
service.log.Error("Failed to delete expired dashboard versions", "error", err.Error())
} else {
service.log.Debug("Deleted old/expired dashboard versions", "rows affected", cmd.DeletedRows)
}
}
func (service *CleanUpService) deleteOldLoginAttempts() {

View File

@@ -0,0 +1,256 @@
package dashboards
import (
"strings"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/util"
)
// DashboardService service for operating on dashboards
type DashboardService interface {
SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error)
}
// DashboardProvisioningService service for operating on provisioned dashboards
type DashboardProvisioningService interface {
SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error)
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
}
// NewService factory for creating a new dashboard service
var NewService = func() DashboardService {
return &dashboardServiceImpl{}
}
// NewProvisioningService factory for creating a new dashboard provisioning service
var NewProvisioningService = func() DashboardProvisioningService {
return &dashboardServiceImpl{}
}
type SaveDashboardDTO struct {
OrgId int64
UpdatedAt time.Time
User *models.SignedInUser
Message string
Overwrite bool
Dashboard *models.Dashboard
}
type dashboardServiceImpl struct {
orgId int64
user *models.SignedInUser
}
func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
cmd := &models.GetProvisionedDashboardDataQuery{Name: name}
err := bus.Dispatch(cmd)
if err != nil {
return nil, err
}
return cmd.Result, nil
}
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool) (*models.SaveDashboardCommand, error) {
dash := dto.Dashboard
dash.Title = strings.TrimSpace(dash.Title)
dash.Data.Set("title", dash.Title)
dash.SetUid(strings.TrimSpace(dash.Uid))
if dash.Title == "" {
return nil, models.ErrDashboardTitleEmpty
}
if dash.IsFolder && dash.FolderId > 0 {
return nil, models.ErrDashboardFolderCannotHaveParent
}
if dash.IsFolder && strings.ToLower(dash.Title) == strings.ToLower(models.RootFolderName) {
return nil, models.ErrDashboardFolderNameExists
}
if !util.IsValidShortUid(dash.Uid) {
return nil, models.ErrDashboardInvalidUid
} else if len(dash.Uid) > 40 {
return nil, models.ErrDashboardUidToLong
}
if validateAlerts {
validateAlertsCmd := models.ValidateDashboardAlertsCommand{
OrgId: dto.OrgId,
Dashboard: dash,
}
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
return nil, models.ErrDashboardContainsInvalidAlertData
}
}
validateBeforeSaveCmd := models.ValidateDashboardBeforeSaveCommand{
OrgId: dto.OrgId,
Dashboard: dash,
Overwrite: dto.Overwrite,
}
if err := bus.Dispatch(&validateBeforeSaveCmd); err != nil {
return nil, err
}
guard := guardian.New(dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
if canSave, err := guard.CanSave(); err != nil || !canSave {
if err != nil {
return nil, err
}
return nil, models.ErrDashboardUpdateAccessDenied
}
cmd := &models.SaveDashboardCommand{
Dashboard: dash.Data,
Message: dto.Message,
OrgId: dto.OrgId,
Overwrite: dto.Overwrite,
UserId: dto.User.UserId,
FolderId: dash.FolderId,
IsFolder: dash.IsFolder,
PluginId: dash.PluginId,
}
if !dto.UpdatedAt.IsZero() {
cmd.UpdatedAt = dto.UpdatedAt
}
return cmd, nil
}
func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
alertCmd := models.UpdateDashboardAlertsCommand{
OrgId: dto.OrgId,
UserId: dto.User.UserId,
Dashboard: cmd.Result,
}
if err := bus.Dispatch(&alertCmd); err != nil {
return models.ErrDashboardFailedToUpdateAlertData
}
return nil
}
func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
dto.User = &models.SignedInUser{
UserId: 0,
OrgRole: models.ROLE_ADMIN,
}
cmd, err := dr.buildSaveDashboardCommand(dto, true)
if err != nil {
return nil, err
}
saveCmd := &models.SaveProvisionedDashboardCommand{
DashboardCmd: cmd,
DashboardProvisioning: provisioning,
}
// dashboard
err = bus.Dispatch(saveCmd)
if err != nil {
return nil, err
}
//alerts
err = dr.updateAlerting(cmd, dto)
if err != nil {
return nil, err
}
return cmd.Result, nil
}
func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDashboardDTO) (*models.Dashboard, error) {
dto.User = &models.SignedInUser{
UserId: 0,
OrgRole: models.ROLE_ADMIN,
}
cmd, err := dr.buildSaveDashboardCommand(dto, false)
if err != nil {
return nil, err
}
err = bus.Dispatch(cmd)
if err != nil {
return nil, err
}
err = dr.updateAlerting(cmd, dto)
if err != nil {
return nil, err
}
return cmd.Result, nil
}
func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
cmd, err := dr.buildSaveDashboardCommand(dto, true)
if err != nil {
return nil, err
}
err = bus.Dispatch(cmd)
if err != nil {
return nil, err
}
err = dr.updateAlerting(cmd, dto)
if err != nil {
return nil, err
}
return cmd.Result, nil
}
func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
cmd, err := dr.buildSaveDashboardCommand(dto, false)
if err != nil {
return nil, err
}
err = bus.Dispatch(cmd)
if err != nil {
return nil, err
}
return cmd.Result, nil
}
type FakeDashboardService struct {
SaveDashboardResult *models.Dashboard
SaveDashboardError error
SavedDashboards []*SaveDashboardDTO
}
func (s *FakeDashboardService) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
s.SavedDashboards = append(s.SavedDashboards, dto)
if s.SaveDashboardResult == nil && s.SaveDashboardError == nil {
s.SaveDashboardResult = dto.Dashboard
}
return s.SaveDashboardResult, s.SaveDashboardError
}
func (s *FakeDashboardService) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
return s.SaveDashboard(dto)
}
func MockDashboardService(mock *FakeDashboardService) {
NewService = func() DashboardService {
return mock
}
}

View File

@@ -0,0 +1,95 @@
package dashboards
import (
"errors"
"testing"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestDashboardService(t *testing.T) {
Convey("Dashboard service tests", t, func() {
service := dashboardServiceImpl{}
origNewDashboardGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
Convey("Save dashboard validation", func() {
dto := &SaveDashboardDTO{}
Convey("When saving a dashboard with empty title it should return error", func() {
titles := []string{"", " ", " \t "}
for _, title := range titles {
dto.Dashboard = models.NewDashboard(title)
_, err := service.SaveDashboard(dto)
So(err, ShouldEqual, models.ErrDashboardTitleEmpty)
}
})
Convey("Should return validation error if it's a folder and have a folder id", func() {
dto.Dashboard = models.NewDashboardFolder("Folder")
dto.Dashboard.FolderId = 1
_, err := service.SaveDashboard(dto)
So(err, ShouldEqual, models.ErrDashboardFolderCannotHaveParent)
})
Convey("Should return validation error if folder is named General", func() {
dto.Dashboard = models.NewDashboardFolder("General")
_, err := service.SaveDashboard(dto)
So(err, ShouldEqual, models.ErrDashboardFolderNameExists)
})
Convey("When saving a dashboard should validate uid", func() {
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
return nil
})
testCases := []struct {
Uid string
Error error
}{
{Uid: "", Error: nil},
{Uid: " ", Error: nil},
{Uid: " \t ", Error: nil},
{Uid: "asdf90_-", Error: nil},
{Uid: "asdf/90", Error: models.ErrDashboardInvalidUid},
{Uid: " asdfghjklqwertyuiopzxcvbnmasdfghjklqwer ", Error: nil},
{Uid: "asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm", Error: models.ErrDashboardUidToLong},
}
for _, tc := range testCases {
dto.Dashboard = models.NewDashboard("title")
dto.Dashboard.SetUid(tc.Uid)
dto.User = &models.SignedInUser{}
_, err := service.buildSaveDashboardCommand(dto, true)
So(err, ShouldEqual, tc.Error)
}
})
Convey("Should return validation error if alert data is invalid", func() {
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return errors.New("error")
})
dto.Dashboard = models.NewDashboard("Dash")
_, err := service.SaveDashboard(dto)
So(err, ShouldEqual, models.ErrDashboardContainsInvalidAlertData)
})
})
Reset(func() {
guardian.New = origNewDashboardGuardian
})
})
}

View File

@@ -1,138 +0,0 @@
package dashboards
import (
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
)
type Repository interface {
SaveDashboard(*SaveDashboardDTO) (*models.Dashboard, error)
SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error)
GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error)
}
var repositoryInstance Repository
func GetRepository() Repository {
return repositoryInstance
}
func SetRepository(rep Repository) {
repositoryInstance = rep
}
type SaveDashboardDTO struct {
OrgId int64
UpdatedAt time.Time
UserId int64
Message string
Overwrite bool
Dashboard *models.Dashboard
}
type DashboardRepository struct{}
func (dr *DashboardRepository) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
cmd := &models.GetProvisionedDashboardDataQuery{Name: name}
err := bus.Dispatch(cmd)
if err != nil {
return nil, err
}
return cmd.Result, nil
}
func (dr *DashboardRepository) buildSaveDashboardCommand(dto *SaveDashboardDTO) (*models.SaveDashboardCommand, error) {
dashboard := dto.Dashboard
if dashboard.Title == "" {
return nil, models.ErrDashboardTitleEmpty
}
validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{
OrgId: dto.OrgId,
Dashboard: dashboard,
}
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
return nil, models.ErrDashboardContainsInvalidAlertData
}
cmd := &models.SaveDashboardCommand{
Dashboard: dashboard.Data,
Message: dto.Message,
OrgId: dto.OrgId,
Overwrite: dto.Overwrite,
UserId: dto.UserId,
FolderId: dashboard.FolderId,
IsFolder: dashboard.IsFolder,
}
if !dto.UpdatedAt.IsZero() {
cmd.UpdatedAt = dto.UpdatedAt
}
return cmd, nil
}
func (dr *DashboardRepository) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error {
alertCmd := alerting.UpdateDashboardAlertsCommand{
OrgId: dto.OrgId,
UserId: dto.UserId,
Dashboard: cmd.Result,
}
if err := bus.Dispatch(&alertCmd); err != nil {
return models.ErrDashboardFailedToUpdateAlertData
}
return nil
}
func (dr *DashboardRepository) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
cmd, err := dr.buildSaveDashboardCommand(dto)
if err != nil {
return nil, err
}
saveCmd := &models.SaveProvisionedDashboardCommand{
DashboardCmd: cmd,
DashboardProvisioning: provisioning,
}
// dashboard
err = bus.Dispatch(saveCmd)
if err != nil {
return nil, err
}
//alerts
err = dr.updateAlerting(cmd, dto)
if err != nil {
return nil, err
}
return cmd.Result, nil
}
func (dr *DashboardRepository) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
cmd, err := dr.buildSaveDashboardCommand(dto)
if err != nil {
return nil, err
}
err = bus.Dispatch(cmd)
if err != nil {
return nil, err
}
err = dr.updateAlerting(cmd, dto)
if err != nil {
return nil, err
}
return cmd.Result, nil
}

View File

@@ -0,0 +1,245 @@
package dashboards
import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/search"
)
// FolderService service for operating on folders
type FolderService interface {
GetFolders(limit int) ([]*models.Folder, error)
GetFolderByID(id int64) (*models.Folder, error)
GetFolderByUID(uid string) (*models.Folder, error)
CreateFolder(cmd *models.CreateFolderCommand) error
UpdateFolder(uid string, cmd *models.UpdateFolderCommand) error
DeleteFolder(uid string) (*models.Folder, error)
}
// NewFolderService factory for creating a new folder service
var NewFolderService = func(orgId int64, user *models.SignedInUser) FolderService {
return &dashboardServiceImpl{
orgId: orgId,
user: user,
}
}
func (dr *dashboardServiceImpl) GetFolders(limit int) ([]*models.Folder, error) {
if limit == 0 {
limit = 1000
}
searchQuery := search.Query{
SignedInUser: dr.user,
DashboardIds: make([]int64, 0),
FolderIds: make([]int64, 0),
Limit: limit,
OrgId: dr.orgId,
Type: "dash-folder",
Permission: models.PERMISSION_VIEW,
}
if err := bus.Dispatch(&searchQuery); err != nil {
return nil, err
}
folders := make([]*models.Folder, 0)
for _, hit := range searchQuery.Result {
folders = append(folders, &models.Folder{
Id: hit.Id,
Uid: hit.Uid,
Title: hit.Title,
})
}
return folders, nil
}
func (dr *dashboardServiceImpl) GetFolderByID(id int64) (*models.Folder, error) {
query := models.GetDashboardQuery{OrgId: dr.orgId, Id: id}
dashFolder, err := getFolder(query)
if err != nil {
return nil, toFolderError(err)
}
g := guardian.New(dashFolder.Id, dr.orgId, dr.user)
if canView, err := g.CanView(); err != nil || !canView {
if err != nil {
return nil, toFolderError(err)
}
return nil, models.ErrFolderAccessDenied
}
return dashToFolder(dashFolder), nil
}
func (dr *dashboardServiceImpl) GetFolderByUID(uid string) (*models.Folder, error) {
query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: uid}
dashFolder, err := getFolder(query)
if err != nil {
return nil, toFolderError(err)
}
g := guardian.New(dashFolder.Id, dr.orgId, dr.user)
if canView, err := g.CanView(); err != nil || !canView {
if err != nil {
return nil, toFolderError(err)
}
return nil, models.ErrFolderAccessDenied
}
return dashToFolder(dashFolder), nil
}
func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) error {
dashFolder := cmd.GetDashboardModel(dr.orgId, dr.user.UserId)
dto := &SaveDashboardDTO{
Dashboard: dashFolder,
OrgId: dr.orgId,
User: dr.user,
}
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
if err != nil {
return toFolderError(err)
}
err = bus.Dispatch(saveDashboardCmd)
if err != nil {
return toFolderError(err)
}
query := models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id}
dashFolder, err = getFolder(query)
if err != nil {
return toFolderError(err)
}
cmd.Result = dashToFolder(dashFolder)
return nil
}
func (dr *dashboardServiceImpl) UpdateFolder(existingUid string, cmd *models.UpdateFolderCommand) error {
query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: existingUid}
dashFolder, err := getFolder(query)
if err != nil {
return toFolderError(err)
}
cmd.UpdateDashboardModel(dashFolder, dr.orgId, dr.user.UserId)
dto := &SaveDashboardDTO{
Dashboard: dashFolder,
OrgId: dr.orgId,
User: dr.user,
Overwrite: cmd.Overwrite,
}
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
if err != nil {
return toFolderError(err)
}
err = bus.Dispatch(saveDashboardCmd)
if err != nil {
return toFolderError(err)
}
query = models.GetDashboardQuery{OrgId: dr.orgId, Id: saveDashboardCmd.Result.Id}
dashFolder, err = getFolder(query)
if err != nil {
return toFolderError(err)
}
cmd.Result = dashToFolder(dashFolder)
return nil
}
func (dr *dashboardServiceImpl) DeleteFolder(uid string) (*models.Folder, error) {
query := models.GetDashboardQuery{OrgId: dr.orgId, Uid: uid}
dashFolder, err := getFolder(query)
if err != nil {
return nil, toFolderError(err)
}
guardian := guardian.New(dashFolder.Id, dr.orgId, dr.user)
if canSave, err := guardian.CanSave(); err != nil || !canSave {
if err != nil {
return nil, toFolderError(err)
}
return nil, models.ErrFolderAccessDenied
}
deleteCmd := models.DeleteDashboardCommand{OrgId: dr.orgId, Id: dashFolder.Id}
if err := bus.Dispatch(&deleteCmd); err != nil {
return nil, toFolderError(err)
}
return dashToFolder(dashFolder), nil
}
func getFolder(query models.GetDashboardQuery) (*models.Dashboard, error) {
if err := bus.Dispatch(&query); err != nil {
return nil, toFolderError(err)
}
if !query.Result.IsFolder {
return nil, models.ErrFolderNotFound
}
return query.Result, nil
}
func dashToFolder(dash *models.Dashboard) *models.Folder {
return &models.Folder{
Id: dash.Id,
Uid: dash.Uid,
Title: dash.Title,
HasAcl: dash.HasAcl,
Url: dash.GetUrl(),
Version: dash.Version,
Created: dash.Created,
CreatedBy: dash.CreatedBy,
Updated: dash.Updated,
UpdatedBy: dash.UpdatedBy,
}
}
func toFolderError(err error) error {
if err == models.ErrDashboardTitleEmpty {
return models.ErrFolderTitleEmpty
}
if err == models.ErrDashboardUpdateAccessDenied {
return models.ErrFolderAccessDenied
}
if err == models.ErrDashboardWithSameNameInFolderExists {
return models.ErrFolderSameNameExists
}
if err == models.ErrDashboardWithSameUIDExists {
return models.ErrFolderWithSameUIDExists
}
if err == models.ErrDashboardVersionMismatch {
return models.ErrFolderVersionMismatch
}
if err == models.ErrDashboardNotFound {
return models.ErrFolderNotFound
}
if err == models.ErrDashboardFailedGenerateUniqueUid {
err = models.ErrFolderFailedGenerateUniqueUid
}
return err
}

View File

@@ -0,0 +1,191 @@
package dashboards
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
. "github.com/smartystreets/goconvey/convey"
)
func TestFolderService(t *testing.T) {
Convey("Folder service tests", t, func() {
service := dashboardServiceImpl{
orgId: 1,
user: &models.SignedInUser{UserId: 1},
}
Convey("Given user has no permissions", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{})
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
query.Result = models.NewDashboardFolder("Folder")
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
return models.ErrDashboardUpdateAccessDenied
})
Convey("When get folder by id should return access denied error", func() {
_, err := service.GetFolderByID(1)
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrFolderAccessDenied)
})
Convey("When get folder by uid should return access denied error", func() {
_, err := service.GetFolderByUID("uid")
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrFolderAccessDenied)
})
Convey("When creating folder should return access denied error", func() {
err := service.CreateFolder(&models.CreateFolderCommand{
Title: "Folder",
})
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrFolderAccessDenied)
})
Convey("When updating folder should return access denied error", func() {
err := service.UpdateFolder("uid", &models.UpdateFolderCommand{
Uid: "uid",
Title: "Folder",
})
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrFolderAccessDenied)
})
Convey("When deleting folder by uid should return access denied error", func() {
_, err := service.DeleteFolder("uid")
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrFolderAccessDenied)
})
Reset(func() {
guardian.New = origNewGuardian
})
})
Convey("Given user has permission to save", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
dash := models.NewDashboardFolder("Folder")
dash.Id = 1
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
query.Result = dash
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.SaveDashboardCommand) error {
cmd.Result = dash
return nil
})
bus.AddHandler("test", func(cmd *models.DeleteDashboardCommand) error {
return nil
})
Convey("When creating folder should not return access denied error", func() {
err := service.CreateFolder(&models.CreateFolderCommand{
Title: "Folder",
})
So(err, ShouldBeNil)
})
Convey("When updating folder should not return access denied error", func() {
err := service.UpdateFolder("uid", &models.UpdateFolderCommand{
Uid: "uid",
Title: "Folder",
})
So(err, ShouldBeNil)
})
Convey("When deleting folder by uid should not return access denied error", func() {
_, err := service.DeleteFolder("uid")
So(err, ShouldBeNil)
})
Reset(func() {
guardian.New = origNewGuardian
})
})
Convey("Given user has permission to view", func() {
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true})
dashFolder := models.NewDashboardFolder("Folder")
dashFolder.Id = 1
dashFolder.Uid = "uid-abc"
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
query.Result = dashFolder
return nil
})
Convey("When get folder by id should return folder", func() {
f, _ := service.GetFolderByID(1)
So(f.Id, ShouldEqual, dashFolder.Id)
So(f.Uid, ShouldEqual, dashFolder.Uid)
So(f.Title, ShouldEqual, dashFolder.Title)
})
Convey("When get folder by uid should return folder", func() {
f, _ := service.GetFolderByUID("uid")
So(f.Id, ShouldEqual, dashFolder.Id)
So(f.Uid, ShouldEqual, dashFolder.Uid)
So(f.Title, ShouldEqual, dashFolder.Title)
})
Reset(func() {
guardian.New = origNewGuardian
})
})
Convey("Should map errors correct", func() {
testCases := []struct {
ActualError error
ExpectedError error
}{
{ActualError: models.ErrDashboardTitleEmpty, ExpectedError: models.ErrFolderTitleEmpty},
{ActualError: models.ErrDashboardUpdateAccessDenied, ExpectedError: models.ErrFolderAccessDenied},
{ActualError: models.ErrDashboardWithSameNameInFolderExists, ExpectedError: models.ErrFolderSameNameExists},
{ActualError: models.ErrDashboardWithSameUIDExists, ExpectedError: models.ErrFolderWithSameUIDExists},
{ActualError: models.ErrDashboardVersionMismatch, ExpectedError: models.ErrFolderVersionMismatch},
{ActualError: models.ErrDashboardNotFound, ExpectedError: models.ErrFolderNotFound},
{ActualError: models.ErrDashboardFailedGenerateUniqueUid, ExpectedError: models.ErrFolderFailedGenerateUniqueUid},
{ActualError: models.ErrDashboardInvalidUid, ExpectedError: models.ErrDashboardInvalidUid},
}
for _, tc := range testCases {
actualError := toFolderError(tc.ActualError)
if actualError != tc.ExpectedError {
t.Errorf("For error '%s' expected error '%s', actual '%s'", tc.ActualError, tc.ExpectedError, actualError)
}
}
})
})
}

View File

@@ -1,13 +1,31 @@
package guardian
import (
"errors"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
type DashboardGuardian struct {
var (
ErrGuardianPermissionExists = errors.New("Permission already exists")
ErrGuardianOverride = errors.New("You can only override a permission to be higher")
)
// DashboardGuardian to be used for guard against operations without access on dashboard and acl
type DashboardGuardian interface {
CanSave() (bool, error)
CanEdit() (bool, error)
CanView() (bool, error)
CanAdmin() (bool, error)
HasPermission(permission m.PermissionType) (bool, error)
CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error)
GetAcl() ([]*m.DashboardAclInfoDTO, error)
}
type dashboardGuardianImpl struct {
user *m.SignedInUser
dashId int64
orgId int64
@@ -16,8 +34,9 @@ type DashboardGuardian struct {
log log.Logger
}
func NewDashboardGuardian(dashId int64, orgId int64, user *m.SignedInUser) *DashboardGuardian {
return &DashboardGuardian{
// New factory for creating a new dashboard guardian instance
var New = func(dashId int64, orgId int64, user *m.SignedInUser) DashboardGuardian {
return &dashboardGuardianImpl{
user: user,
dashId: dashId,
orgId: orgId,
@@ -25,11 +44,11 @@ func NewDashboardGuardian(dashId int64, orgId int64, user *m.SignedInUser) *Dash
}
}
func (g *DashboardGuardian) CanSave() (bool, error) {
func (g *dashboardGuardianImpl) CanSave() (bool, error) {
return g.HasPermission(m.PERMISSION_EDIT)
}
func (g *DashboardGuardian) CanEdit() (bool, error) {
func (g *dashboardGuardianImpl) CanEdit() (bool, error) {
if setting.ViewersCanEdit {
return g.HasPermission(m.PERMISSION_VIEW)
}
@@ -37,15 +56,15 @@ func (g *DashboardGuardian) CanEdit() (bool, error) {
return g.HasPermission(m.PERMISSION_EDIT)
}
func (g *DashboardGuardian) CanView() (bool, error) {
func (g *dashboardGuardianImpl) CanView() (bool, error) {
return g.HasPermission(m.PERMISSION_VIEW)
}
func (g *DashboardGuardian) CanAdmin() (bool, error) {
func (g *dashboardGuardianImpl) CanAdmin() (bool, error) {
return g.HasPermission(m.PERMISSION_ADMIN)
}
func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, error) {
func (g *dashboardGuardianImpl) HasPermission(permission m.PermissionType) (bool, error) {
if g.user.OrgRole == m.ROLE_ADMIN {
return true, nil
}
@@ -58,7 +77,7 @@ func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, er
return g.checkAcl(permission, acl)
}
func (g *DashboardGuardian) checkAcl(permission m.PermissionType, acl []*m.DashboardAclInfoDTO) (bool, error) {
func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.DashboardAclInfoDTO) (bool, error) {
orgRole := g.user.OrgRole
teamAclItems := []*m.DashboardAclInfoDTO{}
@@ -106,22 +125,59 @@ func (g *DashboardGuardian) checkAcl(permission m.PermissionType, acl []*m.Dashb
return false, nil
}
func (g *DashboardGuardian) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) {
if g.user.OrgRole == m.ROLE_ADMIN {
return true, nil
func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) {
acl := []*m.DashboardAclInfoDTO{}
adminRole := m.ROLE_ADMIN
everyoneWithAdminRole := &m.DashboardAclInfoDTO{DashboardId: g.dashId, UserId: 0, TeamId: 0, Role: &adminRole, Permission: m.PERMISSION_ADMIN}
// validate that duplicate permissions don't exists
for _, p := range updatePermissions {
aclItem := &m.DashboardAclInfoDTO{DashboardId: p.DashboardId, UserId: p.UserId, TeamId: p.TeamId, Role: p.Role, Permission: p.Permission}
if aclItem.IsDuplicateOf(everyoneWithAdminRole) {
return false, ErrGuardianPermissionExists
}
for _, a := range acl {
if a.IsDuplicateOf(aclItem) {
return false, ErrGuardianPermissionExists
}
}
acl = append(acl, aclItem)
}
acl := []*m.DashboardAclInfoDTO{}
existingPermissions, err := g.GetAcl()
if err != nil {
return false, err
}
for _, p := range updatePermissions {
acl = append(acl, &m.DashboardAclInfoDTO{UserId: p.UserId, TeamId: p.TeamId, Role: p.Role, Permission: p.Permission})
// validate overridden permissions to be higher
for _, a := range acl {
for _, existingPerm := range existingPermissions {
// handle default permissions
if existingPerm.DashboardId == -1 {
existingPerm.DashboardId = g.dashId
}
if a.DashboardId == existingPerm.DashboardId {
continue
}
if a.IsDuplicateOf(existingPerm) && a.Permission <= existingPerm.Permission {
return false, ErrGuardianOverride
}
}
}
if g.user.OrgRole == m.ROLE_ADMIN {
return true, nil
}
return g.checkAcl(permission, acl)
}
// GetAcl returns dashboard acl
func (g *DashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
func (g *dashboardGuardianImpl) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
if g.acl != nil {
return g.acl, nil
}
@@ -131,11 +187,18 @@ func (g *DashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
return nil, err
}
for _, a := range query.Result {
// handle default permissions
if a.DashboardId == -1 {
a.DashboardId = g.dashId
}
}
g.acl = query.Result
return g.acl, nil
}
func (g *DashboardGuardian) getTeams() ([]*m.Team, error) {
func (g *dashboardGuardianImpl) getTeams() ([]*m.Team, error) {
if g.groups != nil {
return g.groups, nil
}
@@ -146,3 +209,54 @@ func (g *DashboardGuardian) getTeams() ([]*m.Team, error) {
g.groups = query.Result
return query.Result, err
}
type FakeDashboardGuardian struct {
DashId int64
OrgId int64
User *m.SignedInUser
CanSaveValue bool
CanEditValue bool
CanViewValue bool
CanAdminValue bool
HasPermissionValue bool
CheckPermissionBeforeUpdateValue bool
CheckPermissionBeforeUpdateError error
GetAclValue []*m.DashboardAclInfoDTO
}
func (g *FakeDashboardGuardian) CanSave() (bool, error) {
return g.CanSaveValue, nil
}
func (g *FakeDashboardGuardian) CanEdit() (bool, error) {
return g.CanEditValue, nil
}
func (g *FakeDashboardGuardian) CanView() (bool, error) {
return g.CanViewValue, nil
}
func (g *FakeDashboardGuardian) CanAdmin() (bool, error) {
return g.CanAdminValue, nil
}
func (g *FakeDashboardGuardian) HasPermission(permission m.PermissionType) (bool, error) {
return g.HasPermissionValue, nil
}
func (g *FakeDashboardGuardian) CheckPermissionBeforeUpdate(permission m.PermissionType, updatePermissions []*m.DashboardAcl) (bool, error) {
return g.CheckPermissionBeforeUpdateValue, g.CheckPermissionBeforeUpdateError
}
func (g *FakeDashboardGuardian) GetAcl() ([]*m.DashboardAclInfoDTO, error) {
return g.GetAclValue, nil
}
func MockDashboardGuardian(mock *FakeDashboardGuardian) {
New = func(dashId int64, orgId int64, user *m.SignedInUser) DashboardGuardian {
mock.OrgId = orgId
mock.DashId = dashId
mock.User = user
return mock
}
}

View File

@@ -0,0 +1,711 @@
package guardian
import (
"fmt"
"testing"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestGuardian(t *testing.T) {
Convey("Guardian permission tests", t, func() {
orgRoleScenario("Given user has admin org role", m.ROLE_ADMIN, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeTrue)
So(canEdit, ShouldBeTrue)
So(canSave, ShouldBeTrue)
So(canView, ShouldBeTrue)
Convey("When trying to update permissions", func() {
Convey("With duplicate user permissions should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianPermissionExists)
})
Convey("With duplicate team permissions should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_ADMIN},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianPermissionExists)
})
Convey("With duplicate everyone with editor role permission should return error", func() {
r := m.ROLE_EDITOR
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianPermissionExists)
})
Convey("With duplicate everyone with viewer role permission should return error", func() {
r := m.ROLE_VIEWER
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianPermissionExists)
})
Convey("With everyone with admin role permission should return error", func() {
r := m.ROLE_ADMIN
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_ADMIN},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianPermissionExists)
})
})
Convey("Given default permissions", func() {
editor := m.ROLE_EDITOR
viewer := m.ROLE_VIEWER
existingPermissions := []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: -1, Role: &editor, Permission: m.PERMISSION_EDIT},
{OrgId: 1, DashboardId: -1, Role: &viewer, Permission: m.PERMISSION_VIEW},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = existingPermissions
return nil
})
Convey("When trying to update dashboard permissions without everyone with role editor can edit should be allowed", func() {
r := m.ROLE_VIEWER
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_VIEW},
}
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(ok, ShouldBeTrue)
})
Convey("When trying to update dashboard permissions without everyone with role viewer can view should be allowed", func() {
r := m.ROLE_EDITOR
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 1, Role: &r, Permission: m.PERMISSION_EDIT},
}
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(ok, ShouldBeTrue)
})
})
Convey("Given parent folder has user admin permission", func() {
existingPermissions := []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = existingPermissions
return nil
})
Convey("When trying to update dashboard permissions with admin user permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
Convey("When trying to update dashboard permissions with edit user permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
Convey("When trying to update dashboard permissions with view user permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
})
Convey("Given parent folder has user edit permission", func() {
existingPermissions := []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = existingPermissions
return nil
})
Convey("When trying to update dashboard permissions with admin user permission should be allowed", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
}
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(ok, ShouldBeTrue)
})
Convey("When trying to update dashboard permissions with edit user permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
Convey("When trying to update dashboard permissions with view user permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
})
Convey("Given parent folder has user view permission", func() {
existingPermissions := []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = existingPermissions
return nil
})
Convey("When trying to update dashboard permissions with admin user permission should be allowed", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_ADMIN},
}
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(ok, ShouldBeTrue)
})
Convey("When trying to update dashboard permissions with edit user permission should be allowed", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_EDIT},
}
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(ok, ShouldBeTrue)
})
Convey("When trying to update dashboard permissions with view user permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, UserId: 1, Permission: m.PERMISSION_VIEW},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
})
Convey("Given parent folder has team admin permission", func() {
existingPermissions := []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_ADMIN},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = existingPermissions
return nil
})
Convey("When trying to update dashboard permissions with admin team permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
Convey("When trying to update dashboard permissions with edit team permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
Convey("When trying to update dashboard permissions with view team permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
})
Convey("Given parent folder has team edit permission", func() {
existingPermissions := []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_EDIT},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = existingPermissions
return nil
})
Convey("When trying to update dashboard permissions with admin team permission should be allowed", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
}
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(ok, ShouldBeTrue)
})
Convey("When trying to update dashboard permissions with edit team permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
Convey("When trying to update dashboard permissions with view team permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
})
Convey("Given parent folder has team view permission", func() {
existingPermissions := []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 2, TeamId: 1, Permission: m.PERMISSION_VIEW},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = existingPermissions
return nil
})
Convey("When trying to update dashboard permissions with admin team permission should be allowed", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_ADMIN},
}
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(ok, ShouldBeTrue)
})
Convey("When trying to update dashboard permissions with edit team permission should be allowed", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_EDIT},
}
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(ok, ShouldBeTrue)
})
Convey("When trying to update dashboard permissions with view team permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, TeamId: 1, Permission: m.PERMISSION_VIEW},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
})
Convey("Given parent folder has editor role with edit permission", func() {
r := m.ROLE_EDITOR
existingPermissions := []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 2, Role: &r, Permission: m.PERMISSION_EDIT},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = existingPermissions
return nil
})
Convey("When trying to update dashboard permissions with everyone with editor role can admin permission should be allowed", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_ADMIN},
}
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(ok, ShouldBeTrue)
})
Convey("When trying to update dashboard permissions with everyone with editor role can edit permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
Convey("When trying to update dashboard permissions with everyone with editor role can view permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
})
Convey("Given parent folder has editor role with view permission", func() {
r := m.ROLE_EDITOR
existingPermissions := []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 2, Role: &r, Permission: m.PERMISSION_VIEW},
}
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = existingPermissions
return nil
})
Convey("When trying to update dashboard permissions with everyone with viewer role can admin permission should be allowed", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_ADMIN},
}
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(ok, ShouldBeTrue)
})
Convey("When trying to update dashboard permissions with everyone with viewer role can edit permission should be allowed", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_EDIT},
}
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(ok, ShouldBeTrue)
})
Convey("When trying to update dashboard permissions with everyone with viewer role can view permission should return error", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 3, Role: &r, Permission: m.PERMISSION_VIEW},
}
_, err := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(err, ShouldEqual, ErrGuardianOverride)
})
})
})
orgRoleScenario("Given user has editor org role", m.ROLE_EDITOR, func(sc *scenarioContext) {
everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeTrue)
So(canEdit, ShouldBeTrue)
So(canSave, ShouldBeTrue)
So(canView, ShouldBeTrue)
})
everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeTrue)
So(canSave, ShouldBeTrue)
So(canView, ShouldBeTrue)
})
everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeFalse)
So(canSave, ShouldBeFalse)
So(canView, ShouldBeTrue)
})
everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeFalse)
So(canSave, ShouldBeFalse)
So(canView, ShouldBeFalse)
})
everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeFalse)
So(canSave, ShouldBeFalse)
So(canView, ShouldBeFalse)
})
everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeFalse)
So(canSave, ShouldBeFalse)
So(canView, ShouldBeFalse)
})
userWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeTrue)
So(canEdit, ShouldBeTrue)
So(canSave, ShouldBeTrue)
So(canView, ShouldBeTrue)
})
userWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeTrue)
So(canSave, ShouldBeTrue)
So(canView, ShouldBeTrue)
})
userWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeFalse)
So(canSave, ShouldBeFalse)
So(canView, ShouldBeTrue)
})
teamWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeTrue)
So(canEdit, ShouldBeTrue)
So(canSave, ShouldBeTrue)
So(canView, ShouldBeTrue)
})
teamWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeTrue)
So(canSave, ShouldBeTrue)
So(canView, ShouldBeTrue)
})
teamWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeFalse)
So(canSave, ShouldBeFalse)
So(canView, ShouldBeTrue)
})
Convey("When trying to update permissions should return false", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
}
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(ok, ShouldBeFalse)
})
})
orgRoleScenario("Given user has viewer org role", m.ROLE_VIEWER, func(sc *scenarioContext) {
everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeFalse)
So(canSave, ShouldBeFalse)
So(canView, ShouldBeFalse)
})
everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeFalse)
So(canSave, ShouldBeFalse)
So(canView, ShouldBeFalse)
})
everyoneWithRoleScenario(m.ROLE_EDITOR, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeFalse)
So(canSave, ShouldBeFalse)
So(canView, ShouldBeFalse)
})
everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeTrue)
So(canEdit, ShouldBeTrue)
So(canSave, ShouldBeTrue)
So(canView, ShouldBeTrue)
})
everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeTrue)
So(canSave, ShouldBeTrue)
So(canView, ShouldBeTrue)
})
everyoneWithRoleScenario(m.ROLE_VIEWER, m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeFalse)
So(canSave, ShouldBeFalse)
So(canView, ShouldBeTrue)
})
userWithPermissionScenario(m.PERMISSION_ADMIN, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeTrue)
So(canEdit, ShouldBeTrue)
So(canSave, ShouldBeTrue)
So(canView, ShouldBeTrue)
})
userWithPermissionScenario(m.PERMISSION_EDIT, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeTrue)
So(canSave, ShouldBeTrue)
So(canView, ShouldBeTrue)
})
userWithPermissionScenario(m.PERMISSION_VIEW, sc, func(sc *scenarioContext) {
canAdmin, _ := sc.g.CanAdmin()
canEdit, _ := sc.g.CanEdit()
canSave, _ := sc.g.CanSave()
canView, _ := sc.g.CanView()
So(canAdmin, ShouldBeFalse)
So(canEdit, ShouldBeFalse)
So(canSave, ShouldBeFalse)
So(canView, ShouldBeTrue)
})
Convey("When trying to update permissions should return false", func() {
p := []*m.DashboardAcl{
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW},
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN},
}
ok, _ := sc.g.CheckPermissionBeforeUpdate(m.PERMISSION_ADMIN, p)
So(ok, ShouldBeFalse)
})
})
})
}
type scenarioContext struct {
g DashboardGuardian
}
type scenarioFunc func(c *scenarioContext)
func orgRoleScenario(desc string, role m.RoleType, fn scenarioFunc) {
user := &m.SignedInUser{
UserId: 1,
OrgId: 1,
OrgRole: role,
}
guard := New(1, 1, user)
sc := &scenarioContext{
g: guard,
}
Convey(desc, func() {
fn(sc)
})
}
func permissionScenario(desc string, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) {
bus.ClearBusHandlers()
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
query.Result = permissions
return nil
})
teams := []*m.Team{}
for _, p := range permissions {
if p.TeamId > 0 {
teams = append(teams, &m.Team{Id: p.TeamId})
}
}
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
query.Result = teams
return nil
})
Convey(desc, func() {
fn(sc)
})
}
func userWithPermissionScenario(permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
p := []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 1, UserId: 1, Permission: permission},
}
permissionScenario(fmt.Sprintf("and user has permission to %s item", permission), sc, p, fn)
}
func teamWithPermissionScenario(permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
p := []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 1, TeamId: 1, Permission: permission},
}
permissionScenario(fmt.Sprintf("and team has permission to %s item", permission), sc, p, fn)
}
func everyoneWithRoleScenario(role m.RoleType, permission m.PermissionType, sc *scenarioContext, fn scenarioFunc) {
p := []*m.DashboardAclInfoDTO{
{OrgId: 1, DashboardId: 1, UserId: -1, Role: &role, Permission: permission},
}
permissionScenario(fmt.Sprintf("and everyone with %s role can %s item", role, permission), sc, p, fn)
}

View File

@@ -17,7 +17,7 @@ import (
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"gopkg.in/gomail.v2"
gomail "gopkg.in/mail.v2"
)
var mailQueue chan *Message

View File

@@ -2,6 +2,7 @@ package dashboards
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
@@ -14,11 +15,48 @@ type configReader struct {
log log.Logger
}
func (cr *configReader) parseConfigs(file os.FileInfo) ([]*DashboardsAsConfig, error) {
filename, _ := filepath.Abs(filepath.Join(cr.path, file.Name()))
yamlFile, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
apiVersion := &ConfigVersion{ApiVersion: 0}
yaml.Unmarshal(yamlFile, &apiVersion)
if apiVersion.ApiVersion > 0 {
v1 := &DashboardAsConfigV1{}
err := yaml.Unmarshal(yamlFile, &v1)
if err != nil {
return nil, err
}
if v1 != nil {
return v1.mapToDashboardAsConfig(), nil
}
} else {
var v0 []*DashboardsAsConfigV0
err := yaml.Unmarshal(yamlFile, &v0)
if err != nil {
return nil, err
}
if v0 != nil {
cr.log.Warn("[Deprecated] the dashboard provisioning config is outdated. please upgrade", "filename", filename)
return mapV0ToDashboardAsConfig(v0), nil
}
}
return []*DashboardsAsConfig{}, nil
}
func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
var dashboards []*DashboardsAsConfig
files, err := ioutil.ReadDir(cr.path)
if err != nil {
cr.log.Error("cant read dashboard provisioning files from directory", "path", cr.path)
return dashboards, nil
@@ -29,19 +67,14 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
continue
}
filename, _ := filepath.Abs(filepath.Join(cr.path, file.Name()))
yamlFile, err := ioutil.ReadFile(filename)
parsedDashboards, err := cr.parseConfigs(file)
if err != nil {
return nil, err
}
var dashCfg []*DashboardsAsConfig
err = yaml.Unmarshal(yamlFile, &dashCfg)
if err != nil {
return nil, err
if len(parsedDashboards) > 0 {
dashboards = append(dashboards, parsedDashboards...)
}
dashboards = append(dashboards, dashCfg...)
}
for i := range dashboards {

View File

@@ -9,48 +9,33 @@ import (
var (
simpleDashboardConfig string = "./test-configs/dashboards-from-disk"
oldVersion string = "./test-configs/version-0"
brokenConfigs string = "./test-configs/broken-configs"
)
func TestDashboardsAsConfig(t *testing.T) {
Convey("Dashboards as configuration", t, func() {
logger := log.New("test-logger")
Convey("Can read config file", func() {
cfgProvider := configReader{path: simpleDashboardConfig, log: log.New("test-logger")}
Convey("Can read config file version 1 format", func() {
cfgProvider := configReader{path: simpleDashboardConfig, log: logger}
cfg, err := cfgProvider.readConfig()
if err != nil {
t.Fatalf("readConfig return an error %v", err)
}
So(err, ShouldBeNil)
So(len(cfg), ShouldEqual, 2)
validateDashboardAsConfig(cfg)
})
ds := cfg[0]
Convey("Can read config file in version 0 format", func() {
cfgProvider := configReader{path: oldVersion, log: logger}
cfg, err := cfgProvider.readConfig()
So(err, ShouldBeNil)
So(ds.Name, ShouldEqual, "general dashboards")
So(ds.Type, ShouldEqual, "file")
So(ds.OrgId, ShouldEqual, 2)
So(ds.Folder, ShouldEqual, "developers")
So(ds.Editable, ShouldBeTrue)
So(len(ds.Options), ShouldEqual, 1)
So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
ds2 := cfg[1]
So(ds2.Name, ShouldEqual, "default")
So(ds2.Type, ShouldEqual, "file")
So(ds2.OrgId, ShouldEqual, 1)
So(ds2.Folder, ShouldEqual, "")
So(ds2.Editable, ShouldBeFalse)
So(len(ds2.Options), ShouldEqual, 1)
So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
validateDashboardAsConfig(cfg)
})
Convey("Should skip invalid path", func() {
cfgProvider := configReader{path: "/invalid-directory", log: log.New("test-logger")}
cfgProvider := configReader{path: "/invalid-directory", log: logger}
cfg, err := cfgProvider.readConfig()
if err != nil {
t.Fatalf("readConfig return an error %v", err)
@@ -61,7 +46,7 @@ func TestDashboardsAsConfig(t *testing.T) {
Convey("Should skip broken config files", func() {
cfgProvider := configReader{path: brokenConfigs, log: log.New("test-logger")}
cfgProvider := configReader{path: brokenConfigs, log: logger}
cfg, err := cfgProvider.readConfig()
if err != nil {
t.Fatalf("readConfig return an error %v", err)
@@ -71,3 +56,26 @@ func TestDashboardsAsConfig(t *testing.T) {
})
})
}
func validateDashboardAsConfig(cfg []*DashboardsAsConfig) {
So(len(cfg), ShouldEqual, 2)
ds := cfg[0]
So(ds.Name, ShouldEqual, "general dashboards")
So(ds.Type, ShouldEqual, "file")
So(ds.OrgId, ShouldEqual, 2)
So(ds.Folder, ShouldEqual, "developers")
So(ds.Editable, ShouldBeTrue)
So(len(ds.Options), ShouldEqual, 1)
So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
So(ds.DisableDeletion, ShouldBeTrue)
ds2 := cfg[1]
So(ds2.Name, ShouldEqual, "default")
So(ds2.Type, ShouldEqual, "file")
So(ds2.OrgId, ShouldEqual, 1)
So(ds2.Folder, ShouldEqual, "")
So(ds2.Editable, ShouldBeFalse)
So(len(ds2.Options), ShouldEqual, 1)
So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
So(ds2.DisableDeletion, ShouldBeFalse)
}

View File

@@ -25,10 +25,10 @@ var (
)
type fileReader struct {
Cfg *DashboardsAsConfig
Path string
log log.Logger
dashboardRepo dashboards.Repository
Cfg *DashboardsAsConfig
Path string
log log.Logger
dashboardService dashboards.DashboardProvisioningService
}
func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
@@ -48,10 +48,10 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade
}
return &fileReader{
Cfg: cfg,
Path: path,
log: log,
dashboardRepo: dashboards.GetRepository(),
Cfg: cfg,
Path: path,
log: log,
dashboardService: dashboards.NewProvisioningService(),
}, nil
}
@@ -89,12 +89,12 @@ func (fr *fileReader) startWalkingDisk() error {
}
}
folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardRepo)
folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardService)
if err != nil && err != ErrFolderNameMissing {
return err
}
provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardRepo, fr.Cfg.Name)
provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardService, fr.Cfg.Name)
if err != nil {
return err
}
@@ -105,24 +105,7 @@ func (fr *fileReader) startWalkingDisk() error {
return err
}
// find dashboards to delete since json file is missing
var dashboardToDelete []int64
for path, provisioningData := range provisionedDashboardRefs {
_, existsOnDisk := filesFoundOnDisk[path]
if !existsOnDisk {
dashboardToDelete = append(dashboardToDelete, provisioningData.DashboardId)
}
}
// delete dashboard that are missing json file
for _, dashboardId := range dashboardToDelete {
fr.log.Debug("deleting provisioned dashboard. missing on disk", "id", dashboardId)
cmd := &models.DeleteDashboardCommand{OrgId: fr.Cfg.OrgId, Id: dashboardId}
err := bus.Dispatch(cmd)
if err != nil {
fr.log.Error("failed to delete dashboard", "id", cmd.Id)
}
}
fr.deleteDashboardIfFileIsMissing(provisionedDashboardRefs, filesFoundOnDisk)
sanityChecker := newProvisioningSanityChecker(fr.Cfg.Name)
@@ -138,6 +121,29 @@ func (fr *fileReader) startWalkingDisk() error {
return nil
}
func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) {
if fr.Cfg.DisableDeletion {
return
}
// find dashboards to delete since json file is missing
var dashboardToDelete []int64
for path, provisioningData := range provisionedDashboardRefs {
_, existsOnDisk := filesFoundOnDisk[path]
if !existsOnDisk {
dashboardToDelete = append(dashboardToDelete, provisioningData.DashboardId)
}
}
// delete dashboard that are missing json file
for _, dashboardId := range dashboardToDelete {
fr.log.Debug("deleting provisioned dashboard. missing on disk", "id", dashboardId)
cmd := &models.DeleteDashboardCommand{OrgId: fr.Cfg.OrgId, Id: dashboardId}
err := bus.Dispatch(cmd)
if err != nil {
fr.log.Error("failed to delete dashboard", "id", cmd.Id)
}
}
}
func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) (provisioningMetadata, error) {
provisioningMetadata := provisioningMetadata{}
@@ -147,7 +153,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
}
provisionedData, alreadyProvisioned := provisionedDashboardRefs[path]
upToDate := alreadyProvisioned && provisionedData.Updated.Unix() == resolvedFileInfo.ModTime().Unix()
upToDate := alreadyProvisioned && provisionedData.Updated == resolvedFileInfo.ModTime().Unix()
dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId)
if err != nil {
@@ -164,8 +170,8 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
}
if dash.Dashboard.Id != 0 {
fr.log.Error("provisioned dashboard json files cannot contain id")
return provisioningMetadata, nil
dash.Dashboard.Data.Set("id", nil)
dash.Dashboard.Id = 0
}
if alreadyProvisioned {
@@ -173,13 +179,13 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
}
fr.log.Debug("saving new dashboard", "file", path)
dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime()}
_, err = fr.dashboardRepo.SaveProvisionedDashboard(dash, dp)
dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime().Unix()}
_, err = fr.dashboardService.SaveProvisionedDashboard(dash, dp)
return provisioningMetadata, err
}
func getProvisionedDashboardByPath(repo dashboards.Repository, name string) (map[string]*models.DashboardProvisioning, error) {
arr, err := repo.GetProvisionedDashboardData(name)
func getProvisionedDashboardByPath(service dashboards.DashboardProvisioningService, name string) (map[string]*models.DashboardProvisioning, error) {
arr, err := service.GetProvisionedDashboardData(name)
if err != nil {
return nil, err
}
@@ -192,7 +198,7 @@ func getProvisionedDashboardByPath(repo dashboards.Repository, name string) (map
return byPath, nil
}
func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (int64, error) {
func getOrCreateFolderId(cfg *DashboardsAsConfig, service dashboards.DashboardProvisioningService) (int64, error) {
if cfg.Folder == "" {
return 0, ErrFolderNameMissing
}
@@ -207,11 +213,11 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (i
// dashboard folder not found. create one.
if err == models.ErrDashboardNotFound {
dash := &dashboards.SaveDashboardDTO{}
dash.Dashboard = models.NewDashboard(cfg.Folder)
dash.Dashboard = models.NewDashboardFolder(cfg.Folder)
dash.Dashboard.IsFolder = true
dash.Overwrite = true
dash.OrgId = cfg.OrgId
dbDash, err := repo.SaveDashboard(dash)
dbDash, err := service.SaveFolderForProvisionedDashboards(dash)
if err != nil {
return 0, err
}

View File

@@ -15,20 +15,21 @@ import (
)
var (
defaultDashboards string = "./test-dashboards/folder-one"
brokenDashboards string = "./test-dashboards/broken-dashboards"
oneDashboard string = "./test-dashboards/one-dashboard"
defaultDashboards = "./test-dashboards/folder-one"
brokenDashboards = "./test-dashboards/broken-dashboards"
oneDashboard = "./test-dashboards/one-dashboard"
containingId = "./test-dashboards/containing-id"
fakeRepo *fakeDashboardRepo
fakeService *fakeDashboardProvisioningService
)
func TestDashboardFileReader(t *testing.T) {
Convey("Dashboard file reader", t, func() {
bus.ClearBusHandlers()
fakeRepo = &fakeDashboardRepo{}
origNewDashboardProvisioningService := dashboards.NewProvisioningService
fakeService = mockDashboardProvisioningService()
bus.AddHandler("test", mockGetDashboardQuery)
dashboards.SetRepository(fakeRepo)
logger := log.New("test.logger")
Convey("Reading dashboards from disk", func() {
@@ -54,7 +55,7 @@ func TestDashboardFileReader(t *testing.T) {
folders := 0
dashboards := 0
for _, i := range fakeRepo.inserted {
for _, i := range fakeService.inserted {
if i.Dashboard.IsFolder {
folders++
} else {
@@ -71,7 +72,7 @@ func TestDashboardFileReader(t *testing.T) {
stat, _ := os.Stat(oneDashboard + "/dashboard1.json")
fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{
fakeService.getDashboard = append(fakeService.getDashboard, &models.Dashboard{
Updated: stat.ModTime().AddDate(0, 0, -1),
Slug: "grafana",
})
@@ -82,7 +83,19 @@ func TestDashboardFileReader(t *testing.T) {
err = reader.startWalkingDisk()
So(err, ShouldBeNil)
So(len(fakeRepo.inserted), ShouldEqual, 1)
So(len(fakeService.inserted), ShouldEqual, 1)
})
Convey("Overrides id from dashboard.json files", func() {
cfg.Options["path"] = containingId
reader, err := NewDashboardFileReader(cfg, logger)
So(err, ShouldBeNil)
err = reader.startWalkingDisk()
So(err, ShouldBeNil)
So(len(fakeService.inserted), ShouldEqual, 1)
})
Convey("Invalid configuration should return error", func() {
@@ -116,7 +129,7 @@ func TestDashboardFileReader(t *testing.T) {
},
}
_, err := getOrCreateFolderId(cfg, fakeRepo)
_, err := getOrCreateFolderId(cfg, fakeService)
So(err, ShouldEqual, ErrFolderNameMissing)
})
@@ -131,15 +144,15 @@ func TestDashboardFileReader(t *testing.T) {
},
}
folderId, err := getOrCreateFolderId(cfg, fakeRepo)
folderId, err := getOrCreateFolderId(cfg, fakeService)
So(err, ShouldBeNil)
inserted := false
for _, d := range fakeRepo.inserted {
for _, d := range fakeService.inserted {
if d.Dashboard.IsFolder && d.Dashboard.Id == folderId {
inserted = true
}
}
So(len(fakeRepo.inserted), ShouldEqual, 1)
So(len(fakeService.inserted), ShouldEqual, 1)
So(inserted, ShouldBeTrue)
})
@@ -180,6 +193,10 @@ func TestDashboardFileReader(t *testing.T) {
So(reader.Path, ShouldEqual, defaultDashboards)
})
})
Reset(func() {
dashboards.NewProvisioningService = origNewDashboardProvisioningService
})
})
}
@@ -212,29 +229,37 @@ func (ffi FakeFileInfo) Sys() interface{} {
return nil
}
type fakeDashboardRepo struct {
func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
mock := fakeDashboardProvisioningService{}
dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService {
return &mock
}
return &mock
}
type fakeDashboardProvisioningService struct {
inserted []*dashboards.SaveDashboardDTO
provisioned []*models.DashboardProvisioning
getDashboard []*models.Dashboard
}
func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardDTO) (*models.Dashboard, error) {
repo.inserted = append(repo.inserted, json)
return json.Dashboard, nil
func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
return s.provisioned, nil
}
func (repo *fakeDashboardRepo) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
return repo.provisioned, nil
func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
s.inserted = append(s.inserted, dto)
s.provisioned = append(s.provisioned, provisioning)
return dto.Dashboard, nil
}
func (repo *fakeDashboardRepo) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
repo.inserted = append(repo.inserted, dto)
repo.provisioned = append(repo.provisioned, provisioning)
func (s *fakeDashboardProvisioningService) SaveFolderForProvisionedDashboards(dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) {
s.inserted = append(s.inserted, dto)
return dto.Dashboard, nil
}
func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
for _, d := range fakeRepo.getDashboard {
for _, d := range fakeService.getDashboard {
if d.Slug == cmd.Slug {
cmd.Result = d
return nil

View File

@@ -3,4 +3,4 @@
# folder: ''
# type: file
# options:
# folder: /var/lib/grafana/dashboards
# path: /var/lib/grafana/dashboards

View File

@@ -1,7 +1,11 @@
apiVersion: 1
providers:
- name: 'general dashboards'
org_id: 2
orgId: 2
folder: 'developers'
editable: true
disableDeletion: true
type: file
options:
path: /var/lib/grafana/dashboards

View File

@@ -0,0 +1,10 @@
apiVersion: 1
#providers:
#- name: 'gasdf'
# orgId: 2
# folder: 'developers'
# editable: true
# type: file
# options:
# path: /var/lib/grafana/dashboards

View File

@@ -0,0 +1,13 @@
- name: 'general dashboards'
org_id: 2
folder: 'developers'
editable: true
disableDeletion: true
type: file
options:
path: /var/lib/grafana/dashboards
- name: 'default'
type: file
options:
path: /var/lib/grafana/dashboards

View File

@@ -0,0 +1,68 @@
{
"title": "Grafana1",
"tags": [],
"id": 3,
"style": "dark",
"timezone": "browser",
"editable": true,
"rows": [
{
"title": "New row",
"height": "150px",
"collapse": false,
"editable": true,
"panels": [
{
"id": 1,
"span": 12,
"editable": true,
"type": "text",
"mode": "html",
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
"style": {},
"title": "Welcome to"
}
]
}
],
"nav": [
{
"type": "timepicker",
"collapse": false,
"enable": true,
"status": "Stable",
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
],
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"now": true
}
],
"time": {
"from": "now-6h",
"to": "now"
},
"templating": {
"list": []
},
"version": 5
}

View File

@@ -10,12 +10,41 @@ import (
)
type DashboardsAsConfig struct {
Name string `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"`
OrgId int64 `json:"org_id" yaml:"org_id"`
Folder string `json:"folder" yaml:"folder"`
Editable bool `json:"editable" yaml:"editable"`
Options map[string]interface{} `json:"options" yaml:"options"`
Name string
Type string
OrgId int64
Folder string
Editable bool
Options map[string]interface{}
DisableDeletion bool
}
type DashboardsAsConfigV0 struct {
Name string `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"`
OrgId int64 `json:"org_id" yaml:"org_id"`
Folder string `json:"folder" yaml:"folder"`
Editable bool `json:"editable" yaml:"editable"`
Options map[string]interface{} `json:"options" yaml:"options"`
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
}
type ConfigVersion struct {
ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"`
}
type DashboardAsConfigV1 struct {
Providers []*DashboardProviderConfigs `json:"providers" yaml:"providers"`
}
type DashboardProviderConfigs struct {
Name string `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"`
OrgId int64 `json:"orgId" yaml:"orgId"`
Folder string `json:"folder" yaml:"folder"`
Editable bool `json:"editable" yaml:"editable"`
Options map[string]interface{} `json:"options" yaml:"options"`
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
}
func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) {
@@ -36,3 +65,39 @@ func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *Das
return dash, nil
}
func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig {
var r []*DashboardsAsConfig
for _, v := range v0 {
r = append(r, &DashboardsAsConfig{
Name: v.Name,
Type: v.Type,
OrgId: v.OrgId,
Folder: v.Folder,
Editable: v.Editable,
Options: v.Options,
DisableDeletion: v.DisableDeletion,
})
}
return r
}
func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig {
var r []*DashboardsAsConfig
for _, v := range dc.Providers {
r = append(r, &DashboardsAsConfig{
Name: v.Name,
Type: v.Type,
OrgId: v.OrgId,
Folder: v.Folder,
Editable: v.Editable,
Options: v.Options,
DisableDeletion: v.DisableDeletion,
})
}
return r
}

View File

@@ -0,0 +1,113 @@
package datasources
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/grafana/grafana/pkg/log"
"gopkg.in/yaml.v2"
)
type configReader struct {
log log.Logger
}
func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) {
var datasources []*DatasourcesAsConfig
files, err := ioutil.ReadDir(path)
if err != nil {
cr.log.Error("cant read datasource provisioning files from directory", "path", path)
return datasources, nil
}
for _, file := range files {
if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") {
datasource, err := cr.parseDatasourceConfig(path, file)
if err != nil {
return nil, err
}
if datasource != nil {
datasources = append(datasources, datasource)
}
}
}
err = validateDefaultUniqueness(datasources)
if err != nil {
return nil, err
}
return datasources, nil
}
func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*DatasourcesAsConfig, error) {
filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
yamlFile, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
var apiVersion *ConfigVersion
err = yaml.Unmarshal(yamlFile, &apiVersion)
if err != nil {
return nil, err
}
if apiVersion == nil {
apiVersion = &ConfigVersion{ApiVersion: 0}
}
if apiVersion.ApiVersion > 0 {
var v1 *DatasourcesAsConfigV1
err = yaml.Unmarshal(yamlFile, &v1)
if err != nil {
return nil, err
}
return v1.mapToDatasourceFromConfig(apiVersion.ApiVersion), nil
}
var v0 *DatasourcesAsConfigV0
err = yaml.Unmarshal(yamlFile, &v0)
if err != nil {
return nil, err
}
cr.log.Warn("[Deprecated] the datasource provisioning config is outdated. please upgrade", "filename", filename)
return v0.mapToDatasourceFromConfig(apiVersion.ApiVersion), nil
}
func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error {
defaultCount := 0
for i := range datasources {
if datasources[i].Datasources == nil {
continue
}
for _, ds := range datasources[i].Datasources {
if ds.OrgId == 0 {
ds.OrgId = 1
}
if ds.IsDefault {
defaultCount++
if defaultCount > 1 {
return ErrInvalidConfigToManyDefault
}
}
}
for _, ds := range datasources[i].DeleteDatasources {
if ds.OrgId == 0 {
ds.OrgId = 1
}
}
}
return nil
}

View File

@@ -17,6 +17,7 @@ var (
twoDatasourcesConfigPurgeOthers string = "./test-configs/insert-two-delete-two"
doubleDatasourcesConfig string = "./test-configs/double-default"
allProperties string = "./test-configs/all-properties"
versionZero string = "./test-configs/version-0"
brokenYaml string = "./test-configs/broken-yaml"
fakeRepo *fakeRepository
@@ -130,48 +131,86 @@ func TestDatasourceAsConfig(t *testing.T) {
So(len(cfg), ShouldEqual, 0)
})
Convey("can read all properties", func() {
Convey("can read all properties from version 1", func() {
cfgProvifer := &configReader{log: log.New("test logger")}
cfg, err := cfgProvifer.readConfig(allProperties)
if err != nil {
t.Fatalf("readConfig return an error %v", err)
}
So(len(cfg), ShouldEqual, 2)
So(len(cfg), ShouldEqual, 3)
dsCfg := cfg[0]
ds := dsCfg.Datasources[0]
So(ds.Name, ShouldEqual, "name")
So(ds.Type, ShouldEqual, "type")
So(ds.Access, ShouldEqual, models.DS_ACCESS_PROXY)
So(ds.OrgId, ShouldEqual, 2)
So(ds.Url, ShouldEqual, "url")
So(ds.User, ShouldEqual, "user")
So(ds.Password, ShouldEqual, "password")
So(ds.Database, ShouldEqual, "database")
So(ds.BasicAuth, ShouldBeTrue)
So(ds.BasicAuthUser, ShouldEqual, "basic_auth_user")
So(ds.BasicAuthPassword, ShouldEqual, "basic_auth_password")
So(ds.WithCredentials, ShouldBeTrue)
So(ds.IsDefault, ShouldBeTrue)
So(ds.Editable, ShouldBeTrue)
So(dsCfg.ApiVersion, ShouldEqual, 1)
So(len(ds.JsonData), ShouldBeGreaterThan, 2)
So(ds.JsonData["graphiteVersion"], ShouldEqual, "1.1")
So(ds.JsonData["tlsAuth"], ShouldEqual, true)
So(ds.JsonData["tlsAuthWithCACert"], ShouldEqual, true)
validateDatasource(dsCfg)
validateDeleteDatasources(dsCfg)
So(len(ds.SecureJsonData), ShouldBeGreaterThan, 2)
So(ds.SecureJsonData["tlsCACert"], ShouldEqual, "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA==")
So(ds.SecureJsonData["tlsClientCert"], ShouldEqual, "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ==")
So(ds.SecureJsonData["tlsClientKey"], ShouldEqual, "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==")
dsCount := 0
delDsCount := 0
dstwo := cfg[1].Datasources[0]
So(dstwo.Name, ShouldEqual, "name2")
for _, c := range cfg {
dsCount += len(c.Datasources)
delDsCount += len(c.DeleteDatasources)
}
So(dsCount, ShouldEqual, 2)
So(delDsCount, ShouldEqual, 1)
})
Convey("can read all properties from version 0", func() {
cfgProvifer := &configReader{log: log.New("test logger")}
cfg, err := cfgProvifer.readConfig(versionZero)
if err != nil {
t.Fatalf("readConfig return an error %v", err)
}
So(len(cfg), ShouldEqual, 1)
dsCfg := cfg[0]
So(dsCfg.ApiVersion, ShouldEqual, 0)
validateDatasource(dsCfg)
validateDeleteDatasources(dsCfg)
})
})
}
func validateDeleteDatasources(dsCfg *DatasourcesAsConfig) {
So(len(dsCfg.DeleteDatasources), ShouldEqual, 1)
deleteDs := dsCfg.DeleteDatasources[0]
So(deleteDs.Name, ShouldEqual, "old-graphite3")
So(deleteDs.OrgId, ShouldEqual, 2)
}
func validateDatasource(dsCfg *DatasourcesAsConfig) {
ds := dsCfg.Datasources[0]
So(ds.Name, ShouldEqual, "name")
So(ds.Type, ShouldEqual, "type")
So(ds.Access, ShouldEqual, models.DS_ACCESS_PROXY)
So(ds.OrgId, ShouldEqual, 2)
So(ds.Url, ShouldEqual, "url")
So(ds.User, ShouldEqual, "user")
So(ds.Password, ShouldEqual, "password")
So(ds.Database, ShouldEqual, "database")
So(ds.BasicAuth, ShouldBeTrue)
So(ds.BasicAuthUser, ShouldEqual, "basic_auth_user")
So(ds.BasicAuthPassword, ShouldEqual, "basic_auth_password")
So(ds.WithCredentials, ShouldBeTrue)
So(ds.IsDefault, ShouldBeTrue)
So(ds.Editable, ShouldBeTrue)
So(ds.Version, ShouldEqual, 10)
So(len(ds.JsonData), ShouldBeGreaterThan, 2)
So(ds.JsonData["graphiteVersion"], ShouldEqual, "1.1")
So(ds.JsonData["tlsAuth"], ShouldEqual, true)
So(ds.JsonData["tlsAuthWithCACert"], ShouldEqual, true)
So(len(ds.SecureJsonData), ShouldBeGreaterThan, 2)
So(ds.SecureJsonData["tlsCACert"], ShouldEqual, "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA==")
So(ds.SecureJsonData["tlsClientCert"], ShouldEqual, "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ==")
So(ds.SecureJsonData["tlsClientKey"], ShouldEqual, "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A==")
}
type fakeRepository struct {
inserted []*models.AddDataSourceCommand

View File

@@ -2,16 +2,12 @@ package datasources
import (
"errors"
"io/ioutil"
"path/filepath"
"strings"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/models"
yaml "gopkg.in/yaml.v2"
)
var (
@@ -94,65 +90,3 @@ func (dc *DatasourceProvisioner) deleteDatasources(dsToDelete []*DeleteDatasourc
return nil
}
type configReader struct {
log log.Logger
}
func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) {
var datasources []*DatasourcesAsConfig
files, err := ioutil.ReadDir(path)
if err != nil {
cr.log.Error("cant read datasource provisioning files from directory", "path", path)
return datasources, nil
}
for _, file := range files {
if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") {
filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
yamlFile, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
var datasource *DatasourcesAsConfig
err = yaml.Unmarshal(yamlFile, &datasource)
if err != nil {
return nil, err
}
if datasource != nil {
datasources = append(datasources, datasource)
}
}
}
defaultCount := 0
for i := range datasources {
if datasources[i].Datasources == nil {
continue
}
for _, ds := range datasources[i].Datasources {
if ds.OrgId == 0 {
ds.OrgId = 1
}
if ds.IsDefault {
defaultCount++
if defaultCount > 1 {
return nil, ErrInvalidConfigToManyDefault
}
}
}
for _, ds := range datasources[i].DeleteDatasources {
if ds.OrgId == 0 {
ds.OrgId = 1
}
}
}
return datasources, nil
}

View File

@@ -1,23 +1,30 @@
apiVersion: 1
datasources:
- name: name
type: type
access: proxy
org_id: 2
orgId: 2
url: url
password: password
user: user
database: database
basic_auth: true
basic_auth_user: basic_auth_user
basic_auth_password: basic_auth_password
with_credentials: true
is_default: true
json_data:
basicAuth: true
basicAuthUser: basic_auth_user
basicAuthPassword: basic_auth_password
withCredentials: true
isDefault: true
jsonData:
graphiteVersion: "1.1"
tlsAuth: true
tlsAuthWithCACert: true
secure_json_data:
secureJsonData:
tlsCACert: "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA=="
tlsClientCert: "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ=="
tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A=="
editable: true
version: 10
deleteDatasources:
- name: old-graphite3
orgId: 2

View File

@@ -0,0 +1,32 @@
# Should not be included
apiVersion: 1
#datasources:
# - name: name
# type: type
# access: proxy
# orgId: 2
# url: url
# password: password
# user: user
# database: database
# basicAuth: true
# basicAuthUser: basic_auth_user
# basicAuthPassword: basic_auth_password
# withCredentials: true
# jsonData:
# graphiteVersion: "1.1"
# tlsAuth: true
# tlsAuthWithCACert: true
# secureJsonData:
# tlsCACert: "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA=="
# tlsClientCert: "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ=="
# tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A=="
# editable: true
# version: 10
#
#deleteDatasources:
# - name: old-graphite3
# orgId: 2

View File

@@ -3,5 +3,5 @@ datasources:
- name: name2
type: type2
access: proxy
org_id: 2
orgId: 2
url: url2

View File

@@ -0,0 +1,28 @@
datasources:
- name: name
type: type
access: proxy
org_id: 2
url: url
password: password
user: user
database: database
basic_auth: true
basic_auth_user: basic_auth_user
basic_auth_password: basic_auth_password
with_credentials: true
is_default: true
json_data:
graphiteVersion: "1.1"
tlsAuth: true
tlsAuthWithCACert: true
secure_json_data:
tlsCACert: "MjNOcW9RdkbUDHZmpco2HCYzVq9dE+i6Yi+gmUJotq5CDA=="
tlsClientCert: "ckN0dGlyMXN503YNfjTcf9CV+GGQneN+xmAclQ=="
tlsClientKey: "ZkN4aG1aNkja/gKAB1wlnKFIsy2SRDq4slrM0A=="
editable: true
version: 10
delete_datasources:
- name: old-graphite3
org_id: 2

View File

@@ -1,22 +1,74 @@
package datasources
import "github.com/grafana/grafana/pkg/models"
import (
"github.com/grafana/grafana/pkg/models"
)
import "github.com/grafana/grafana/pkg/components/simplejson"
type ConfigVersion struct {
ApiVersion int64 `json:"apiVersion" yaml:"apiVersion"`
}
type DatasourcesAsConfig struct {
Datasources []*DataSourceFromConfig `json:"datasources" yaml:"datasources"`
DeleteDatasources []*DeleteDatasourceConfig `json:"delete_datasources" yaml:"delete_datasources"`
ApiVersion int64
Datasources []*DataSourceFromConfig
DeleteDatasources []*DeleteDatasourceConfig
}
type DeleteDatasourceConfig struct {
OrgId int64
Name string
}
type DataSourceFromConfig struct {
OrgId int64
Version int
Name string
Type string
Access string
Url string
Password string
User string
Database string
BasicAuth bool
BasicAuthUser string
BasicAuthPassword string
WithCredentials bool
IsDefault bool
JsonData map[string]interface{}
SecureJsonData map[string]string
Editable bool
}
type DatasourcesAsConfigV0 struct {
ConfigVersion
Datasources []*DataSourceFromConfigV0 `json:"datasources" yaml:"datasources"`
DeleteDatasources []*DeleteDatasourceConfigV0 `json:"delete_datasources" yaml:"delete_datasources"`
}
type DatasourcesAsConfigV1 struct {
ConfigVersion
Datasources []*DataSourceFromConfigV1 `json:"datasources" yaml:"datasources"`
DeleteDatasources []*DeleteDatasourceConfigV1 `json:"deleteDatasources" yaml:"deleteDatasources"`
}
type DeleteDatasourceConfigV0 struct {
OrgId int64 `json:"org_id" yaml:"org_id"`
Name string `json:"name" yaml:"name"`
}
type DataSourceFromConfig struct {
OrgId int64 `json:"org_id" yaml:"org_id"`
Version int `json:"version" yaml:"version"`
type DeleteDatasourceConfigV1 struct {
OrgId int64 `json:"orgId" yaml:"orgId"`
Name string `json:"name" yaml:"name"`
}
type DataSourceFromConfigV0 struct {
OrgId int64 `json:"org_id" yaml:"org_id"`
Version int `json:"version" yaml:"version"`
Name string `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"`
Access string `json:"access" yaml:"access"`
@@ -34,6 +86,108 @@ type DataSourceFromConfig struct {
Editable bool `json:"editable" yaml:"editable"`
}
type DataSourceFromConfigV1 struct {
OrgId int64 `json:"orgId" yaml:"orgId"`
Version int `json:"version" yaml:"version"`
Name string `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"`
Access string `json:"access" yaml:"access"`
Url string `json:"url" yaml:"url"`
Password string `json:"password" yaml:"password"`
User string `json:"user" yaml:"user"`
Database string `json:"database" yaml:"database"`
BasicAuth bool `json:"basicAuth" yaml:"basicAuth"`
BasicAuthUser string `json:"basicAuthUser" yaml:"basicAuthUser"`
BasicAuthPassword string `json:"basicAuthPassword" yaml:"basicAuthPassword"`
WithCredentials bool `json:"withCredentials" yaml:"withCredentials"`
IsDefault bool `json:"isDefault" yaml:"isDefault"`
JsonData map[string]interface{} `json:"jsonData" yaml:"jsonData"`
SecureJsonData map[string]string `json:"secureJsonData" yaml:"secureJsonData"`
Editable bool `json:"editable" yaml:"editable"`
}
func (cfg *DatasourcesAsConfigV1) mapToDatasourceFromConfig(apiVersion int64) *DatasourcesAsConfig {
r := &DatasourcesAsConfig{}
r.ApiVersion = apiVersion
if cfg == nil {
return r
}
for _, ds := range cfg.Datasources {
r.Datasources = append(r.Datasources, &DataSourceFromConfig{
OrgId: ds.OrgId,
Name: ds.Name,
Type: ds.Type,
Access: ds.Access,
Url: ds.Url,
Password: ds.Password,
User: ds.User,
Database: ds.Database,
BasicAuth: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
BasicAuthPassword: ds.BasicAuthPassword,
WithCredentials: ds.WithCredentials,
IsDefault: ds.IsDefault,
JsonData: ds.JsonData,
SecureJsonData: ds.SecureJsonData,
Editable: ds.Editable,
Version: ds.Version,
})
}
for _, ds := range cfg.DeleteDatasources {
r.DeleteDatasources = append(r.DeleteDatasources, &DeleteDatasourceConfig{
OrgId: ds.OrgId,
Name: ds.Name,
})
}
return r
}
func (cfg *DatasourcesAsConfigV0) mapToDatasourceFromConfig(apiVersion int64) *DatasourcesAsConfig {
r := &DatasourcesAsConfig{}
r.ApiVersion = apiVersion
if cfg == nil {
return r
}
for _, ds := range cfg.Datasources {
r.Datasources = append(r.Datasources, &DataSourceFromConfig{
OrgId: ds.OrgId,
Name: ds.Name,
Type: ds.Type,
Access: ds.Access,
Url: ds.Url,
Password: ds.Password,
User: ds.User,
Database: ds.Database,
BasicAuth: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
BasicAuthPassword: ds.BasicAuthPassword,
WithCredentials: ds.WithCredentials,
IsDefault: ds.IsDefault,
JsonData: ds.JsonData,
SecureJsonData: ds.SecureJsonData,
Editable: ds.Editable,
Version: ds.Version,
})
}
for _, ds := range cfg.DeleteDatasources {
r.DeleteDatasources = append(r.DeleteDatasources, &DeleteDatasourceConfig{
OrgId: ds.OrgId,
Name: ds.Name,
})
}
return r
}
func createInsertCommand(ds *DataSourceFromConfig) *models.AddDataSourceCommand {
jsonData := simplejson.New()
if len(ds.JsonData) > 0 {

View File

@@ -0,0 +1,87 @@
package quota
import (
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/session"
"github.com/grafana/grafana/pkg/setting"
)
func QuotaReached(c *m.ReqContext, target string) (bool, error) {
if !setting.Quota.Enabled {
return false, nil
}
// get the list of scopes that this target is valid for. Org, User, Global
scopes, err := m.GetQuotaScopes(target)
if err != nil {
return false, err
}
for _, scope := range scopes {
c.Logger.Debug("Checking quota", "target", target, "scope", scope)
switch scope.Name {
case "global":
if scope.DefaultLimit < 0 {
continue
}
if scope.DefaultLimit == 0 {
return true, nil
}
if target == "session" {
usedSessions := session.GetSessionCount()
if int64(usedSessions) > scope.DefaultLimit {
c.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit)
return true, nil
}
continue
}
query := m.GetGlobalQuotaByTargetQuery{Target: scope.Target}
if err := bus.Dispatch(&query); err != nil {
return true, err
}
if query.Result.Used >= scope.DefaultLimit {
return true, nil
}
case "org":
if !c.IsSignedIn {
continue
}
query := m.GetOrgQuotaByTargetQuery{OrgId: c.OrgId, Target: scope.Target, Default: scope.DefaultLimit}
if err := bus.Dispatch(&query); err != nil {
return true, err
}
if query.Result.Limit < 0 {
continue
}
if query.Result.Limit == 0 {
return true, nil
}
if query.Result.Used >= query.Result.Limit {
return true, nil
}
case "user":
if !c.IsSignedIn || c.UserId == 0 {
continue
}
query := m.GetUserQuotaByTargetQuery{UserId: c.UserId, Target: scope.Target, Default: scope.DefaultLimit}
if err := bus.Dispatch(&query); err != nil {
return true, err
}
if query.Result.Limit < 0 {
continue
}
if query.Result.Limit == 0 {
return true, nil
}
if query.Result.Used >= query.Result.Limit {
return true, nil
}
}
}
return false, nil
}

View File

@@ -0,0 +1,218 @@
// Copyright 2013 Beego Authors
// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package session
import (
"database/sql"
"fmt"
"log"
"sync"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/go-macaron/session"
)
// MysqlStore represents a mysql session store implementation.
type MysqlStore struct {
c *sql.DB
sid string
lock sync.RWMutex
data map[interface{}]interface{}
}
// NewMysqlStore creates and returns a mysql session store.
func NewMysqlStore(c *sql.DB, sid string, kv map[interface{}]interface{}) *MysqlStore {
return &MysqlStore{
c: c,
sid: sid,
data: kv,
}
}
// Set sets value to given key in session.
func (s *MysqlStore) Set(key, val interface{}) error {
s.lock.Lock()
defer s.lock.Unlock()
s.data[key] = val
return nil
}
// Get gets value by given key in session.
func (s *MysqlStore) Get(key interface{}) interface{} {
s.lock.RLock()
defer s.lock.RUnlock()
return s.data[key]
}
// Delete delete a key from session.
func (s *MysqlStore) Delete(key interface{}) error {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.data, key)
return nil
}
// ID returns current session ID.
func (s *MysqlStore) ID() string {
return s.sid
}
// Release releases resource and save data to provider.
func (s *MysqlStore) Release() error {
data, err := session.EncodeGob(s.data)
if err != nil {
return err
}
_, err = s.c.Exec("UPDATE session SET data=?, expiry=? WHERE `key`=?",
data, time.Now().Unix(), s.sid)
return err
}
// Flush deletes all session data.
func (s *MysqlStore) Flush() error {
s.lock.Lock()
defer s.lock.Unlock()
s.data = make(map[interface{}]interface{})
return nil
}
// MysqlProvider represents a mysql session provider implementation.
type MysqlProvider struct {
c *sql.DB
expire int64
}
// Init initializes mysql session provider.
// connStr: username:password@protocol(address)/dbname?param=value
func (p *MysqlProvider) Init(expire int64, connStr string) (err error) {
p.expire = expire
p.c, err = sql.Open("mysql", connStr)
p.c.SetConnMaxLifetime(time.Second * time.Duration(sessionConnMaxLifetime))
if err != nil {
return err
}
return p.c.Ping()
}
// Read returns raw session store by session ID.
func (p *MysqlProvider) Read(sid string) (session.RawStore, error) {
var data []byte
err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data)
if err == sql.ErrNoRows {
_, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)",
sid, "", time.Now().Unix())
}
if err != nil {
return nil, err
}
var kv map[interface{}]interface{}
if len(data) == 0 {
kv = make(map[interface{}]interface{})
} else {
kv, err = session.DecodeGob(data)
if err != nil {
return nil, err
}
}
return NewMysqlStore(p.c, sid, kv), nil
}
// Exist returns true if session with given ID exists.
func (p *MysqlProvider) Exist(sid string) bool {
exists, err := p.queryExists(sid)
if err != nil {
exists, err = p.queryExists(sid)
}
if err != nil {
log.Printf("session/mysql: error checking if session exists: %v", err)
return false
}
return exists
}
func (p *MysqlProvider) queryExists(sid string) (bool, error) {
var data []byte
err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data)
if err != nil && err != sql.ErrNoRows {
return false, err
}
return err != sql.ErrNoRows, nil
}
// Destory deletes a session by session ID.
func (p *MysqlProvider) Destory(sid string) error {
_, err := p.c.Exec("DELETE FROM session WHERE `key`=?", sid)
return err
}
// Regenerate regenerates a session store from old session ID to new one.
func (p *MysqlProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) {
if p.Exist(sid) {
return nil, fmt.Errorf("new sid '%s' already exists", sid)
}
if !p.Exist(oldsid) {
if _, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)",
oldsid, "", time.Now().Unix()); err != nil {
return nil, err
}
}
if _, err = p.c.Exec("UPDATE session SET `key`=? WHERE `key`=?", sid, oldsid); err != nil {
return nil, err
}
return p.Read(sid)
}
// Count counts and returns number of sessions.
func (p *MysqlProvider) Count() (total int) {
if err := p.c.QueryRow("SELECT COUNT(*) AS NUM FROM session").Scan(&total); err != nil {
panic("session/mysql: error counting records: " + err.Error())
}
return total
}
// GC calls GC to clean expired sessions.
func (p *MysqlProvider) GC() {
var err error
if _, err = p.c.Exec("DELETE FROM session WHERE expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire); err != nil {
_, err = p.c.Exec("DELETE FROM session WHERE expiry + ? <= UNIX_TIMESTAMP(NOW())", p.expire)
}
if err != nil {
log.Printf("session/mysql: error garbage collecting: %v", err)
}
}
func init() {
session.Register("mysql", &MysqlProvider{})
}

View File

@@ -0,0 +1,175 @@
package session
import (
"math/rand"
"time"
ms "github.com/go-macaron/session"
_ "github.com/go-macaron/session/memcache"
_ "github.com/go-macaron/session/postgres"
_ "github.com/go-macaron/session/redis"
"github.com/grafana/grafana/pkg/log"
"gopkg.in/macaron.v1"
)
const (
SESS_KEY_USERID = "uid"
SESS_KEY_OAUTH_STATE = "state"
SESS_KEY_APIKEY = "apikey_id" // used for render requests with api keys
SESS_KEY_LASTLDAPSYNC = "last_ldap_sync"
)
var sessionManager *ms.Manager
var sessionOptions *ms.Options
var StartSessionGC func()
var GetSessionCount func() int
var sessionLogger = log.New("session")
var sessionConnMaxLifetime int64
func init() {
StartSessionGC = func() {
sessionManager.GC()
sessionLogger.Debug("Session GC")
time.AfterFunc(time.Duration(sessionOptions.Gclifetime)*time.Second, StartSessionGC)
}
GetSessionCount = func() int {
return sessionManager.Count()
}
}
func Init(options *ms.Options, connMaxLifetime int64) {
var err error
sessionOptions = prepareOptions(options)
sessionConnMaxLifetime = connMaxLifetime
sessionManager, err = ms.NewManager(options.Provider, *options)
if err != nil {
panic(err)
}
// start GC threads after some random seconds
rndSeconds := 10 + rand.Int63n(180)
time.AfterFunc(time.Duration(rndSeconds)*time.Second, StartSessionGC)
}
func prepareOptions(opt *ms.Options) *ms.Options {
if len(opt.Provider) == 0 {
opt.Provider = "memory"
}
if len(opt.ProviderConfig) == 0 {
opt.ProviderConfig = "data/sessions"
}
if len(opt.CookieName) == 0 {
opt.CookieName = "grafana_sess"
}
if len(opt.CookiePath) == 0 {
opt.CookiePath = "/"
}
if opt.Gclifetime == 0 {
opt.Gclifetime = 3600
}
if opt.Maxlifetime == 0 {
opt.Maxlifetime = opt.Gclifetime
}
if opt.IDLength == 0 {
opt.IDLength = 16
}
return opt
}
func GetSession() SessionStore {
return &SessionWrapper{manager: sessionManager}
}
type SessionStore interface {
// Set sets value to given key in session.
Set(interface{}, interface{}) error
// Get gets value by given key in session.
Get(interface{}) interface{}
// Delete deletes a key from session.
Delete(interface{}) interface{}
// ID returns current session ID.
ID() string
// Release releases session resource and save data to provider.
Release() error
// Destory deletes a session.
Destory(*macaron.Context) error
// init
Start(*macaron.Context) error
// RegenerateId regenerates the session id
RegenerateId(*macaron.Context) error
}
type SessionWrapper struct {
session ms.RawStore
manager *ms.Manager
}
func (s *SessionWrapper) Start(c *macaron.Context) error {
// See https://github.com/grafana/grafana/issues/11155 for details on why
// a recover and retry is needed
defer func() error {
if err := recover(); err != nil {
var retryErr error
s.session, retryErr = s.manager.Start(c)
return retryErr
}
return nil
}()
var err error
s.session, err = s.manager.Start(c)
return err
}
func (s *SessionWrapper) RegenerateId(c *macaron.Context) error {
var err error
s.session, err = s.manager.RegenerateId(c)
return err
}
func (s *SessionWrapper) Set(k interface{}, v interface{}) error {
if s.session != nil {
return s.session.Set(k, v)
}
return nil
}
func (s *SessionWrapper) Get(k interface{}) interface{} {
if s.session != nil {
return s.session.Get(k)
}
return nil
}
func (s *SessionWrapper) Delete(k interface{}) interface{} {
if s.session != nil {
return s.session.Delete(k)
}
return nil
}
func (s *SessionWrapper) ID() string {
if s.session != nil {
return s.session.ID()
}
return ""
}
func (s *SessionWrapper) Release() error {
if s.session != nil {
return s.session.Release()
}
return nil
}
func (s *SessionWrapper) Destory(c *macaron.Context) error {
if s.session != nil {
if err := s.manager.Destory(c); err != nil {
return err
}
s.session = nil
}
return nil
}

View File

@@ -10,6 +10,9 @@ import (
m "github.com/grafana/grafana/pkg/models"
)
// timeNow makes it possible to test usage of time
var timeNow = time.Now
func init() {
bus.AddHandler("sql", SaveAlerts)
bus.AddHandler("sql", HandleAlertsQuery)
@@ -61,52 +64,61 @@ func deleteAlertByIdInternal(alertId int64, reason string, sess *DBSession) erro
}
func HandleAlertsQuery(query *m.GetAlertsQuery) error {
var sql bytes.Buffer
params := make([]interface{}, 0)
builder := SqlBuilder{}
sql.WriteString(`SELECT *
from alert
`)
builder.Write(`SELECT
alert.id,
alert.dashboard_id,
alert.panel_id,
alert.name,
alert.state,
alert.new_state_date,
alert.eval_date,
alert.execution_error,
dashboard.uid as dashboard_uid,
dashboard.slug as dashboard_slug
FROM alert
INNER JOIN dashboard on dashboard.id = alert.dashboard_id `)
sql.WriteString(`WHERE org_id = ?`)
params = append(params, query.OrgId)
builder.Write(`WHERE alert.org_id = ?`, query.OrgId)
if query.DashboardId != 0 {
sql.WriteString(` AND dashboard_id = ?`)
params = append(params, query.DashboardId)
builder.Write(` AND alert.dashboard_id = ?`, query.DashboardId)
}
if query.PanelId != 0 {
sql.WriteString(` AND panel_id = ?`)
params = append(params, query.PanelId)
builder.Write(` AND alert.panel_id = ?`, query.PanelId)
}
if len(query.State) > 0 && query.State[0] != "all" {
sql.WriteString(` AND (`)
builder.Write(` AND (`)
for i, v := range query.State {
if i > 0 {
sql.WriteString(" OR ")
builder.Write(" OR ")
}
if strings.HasPrefix(v, "not_") {
sql.WriteString("state <> ? ")
builder.Write("state <> ? ")
v = strings.TrimPrefix(v, "not_")
} else {
sql.WriteString("state = ? ")
builder.Write("state = ? ")
}
params = append(params, v)
builder.AddParams(v)
}
sql.WriteString(")")
builder.Write(")")
}
sql.WriteString(" ORDER BY name ASC")
if query.User.OrgRole != m.ROLE_ADMIN {
builder.writeDashboardPermissionFilter(query.User, m.PERMISSION_EDIT)
}
builder.Write(" ORDER BY name ASC")
if query.Limit != 0 {
sql.WriteString(" LIMIT ?")
params = append(params, query.Limit)
builder.Write(" LIMIT ?", query.Limit)
}
alerts := make([]*m.Alert, 0)
if err := x.SQL(sql.String(), params...).Find(&alerts); err != nil {
alerts := make([]*m.AlertListItemDTO, 0)
if err := x.SQL(builder.GetSqlString(), builder.params...).Find(&alerts); err != nil {
return err
}
@@ -120,7 +132,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
return nil
}
func DeleteAlertDefinition(dashboardId int64, sess *DBSession) error {
func deleteAlertDefinition(dashboardId int64, sess *DBSession) error {
alerts := make([]*m.Alert, 0)
sess.Where("dashboard_id = ?", dashboardId).Find(&alerts)
@@ -138,7 +150,7 @@ func SaveAlerts(cmd *m.SaveAlertsCommand) error {
return err
}
if err := upsertAlerts(existingAlerts, cmd, sess); err != nil {
if err := updateAlerts(existingAlerts, cmd, sess); err != nil {
return err
}
@@ -150,7 +162,7 @@ func SaveAlerts(cmd *m.SaveAlertsCommand) error {
})
}
func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBSession) error {
func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBSession) error {
for _, alert := range cmd.Alerts {
update := false
var alertToUpdate *m.Alert
@@ -166,7 +178,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
if update {
if alertToUpdate.ContainsUpdates(alert) {
alert.Updated = time.Now()
alert.Updated = timeNow()
alert.State = alertToUpdate.State
sess.MustCols("message")
_, err := sess.Id(alert.Id).Update(alert)
@@ -177,10 +189,10 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
sqlog.Debug("Alert updated", "name", alert.Name, "id", alert.Id)
}
} else {
alert.Updated = time.Now()
alert.Created = time.Now()
alert.Updated = timeNow()
alert.Created = timeNow()
alert.State = m.AlertStatePending
alert.NewStateDate = time.Now()
alert.NewStateDate = timeNow()
_, err := sess.Insert(alert)
if err != nil {
@@ -243,8 +255,8 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
}
alert.State = cmd.State
alert.StateChanges += 1
alert.NewStateDate = time.Now()
alert.StateChanges++
alert.NewStateDate = timeNow()
alert.EvalData = cmd.EvalData
if cmd.Error == "" {
@@ -267,11 +279,13 @@ func PauseAlert(cmd *m.PauseAlertCommand) error {
var buffer bytes.Buffer
params := make([]interface{}, 0)
buffer.WriteString(`UPDATE alert SET state = ?`)
buffer.WriteString(`UPDATE alert SET state = ?, new_state_date = ?`)
if cmd.Paused {
params = append(params, string(m.AlertStatePaused))
params = append(params, timeNow())
} else {
params = append(params, string(m.AlertStatePending))
params = append(params, timeNow())
}
buffer.WriteString(` WHERE id IN (?` + strings.Repeat(",?", len(cmd.AlertIds)-1) + `)`)
@@ -297,7 +311,7 @@ func PauseAllAlerts(cmd *m.PauseAllAlertCommand) error {
newState = string(m.AlertStatePending)
}
res, err := sess.Exec(`UPDATE alert SET state = ?`, newState)
res, err := sess.Exec(`UPDATE alert SET state = ?, new_state_date = ?`, newState, timeNow())
if err != nil {
return err
}

View File

@@ -6,9 +6,26 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
"time"
)
func mockTimeNow() {
var timeSeed int64
timeNow = func() time.Time {
fakeNow := time.Unix(timeSeed, 0)
timeSeed += 1
return fakeNow
}
}
func resetTimeNow() {
timeNow = time.Now
}
func TestAlertingDataAccess(t *testing.T) {
mockTimeNow()
defer resetTimeNow()
Convey("Testing Alerting data access", t, func() {
InitTestDB(t)
@@ -50,13 +67,11 @@ func TestAlertingDataAccess(t *testing.T) {
So(err, ShouldBeNil)
})
Convey("can pause alert", func() {
cmd := &m.PauseAllAlertCommand{
Paused: true,
}
alert, _ := getAlertById(1)
stateDateBeforePause := alert.NewStateDate
err = PauseAllAlerts(cmd)
So(err, ShouldBeNil)
Convey("can pause all alerts", func() {
pauseAllAlerts(true)
Convey("cannot updated paused alert", func() {
cmd := &m.SetAlertStateCommand{
@@ -67,19 +82,38 @@ func TestAlertingDataAccess(t *testing.T) {
err = SetAlertState(cmd)
So(err, ShouldNotBeNil)
})
Convey("pausing alerts should update their NewStateDate", func() {
alert, _ = getAlertById(1)
stateDateAfterPause := alert.NewStateDate
So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterPause)
})
Convey("unpausing alerts should update their NewStateDate again", func() {
pauseAllAlerts(false)
alert, _ = getAlertById(1)
stateDateAfterUnpause := alert.NewStateDate
So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterUnpause)
})
})
})
Convey("Can read properties", func() {
alertQuery := m.GetAlertsQuery{DashboardId: testDash.Id, PanelId: 1, OrgId: 1}
alertQuery := m.GetAlertsQuery{DashboardId: testDash.Id, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&alertQuery)
alert := alertQuery.Result[0]
So(err2, ShouldBeNil)
So(alert.Name, ShouldEqual, "Alerting title")
So(alert.Message, ShouldEqual, "Alerting message")
So(alert.State, ShouldEqual, "pending")
So(alert.Frequency, ShouldEqual, 1)
})
Convey("Viewer cannot read alerts", func() {
alertQuery := m.GetAlertsQuery{DashboardId: testDash.Id, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_VIEWER}}
err2 := HandleAlertsQuery(&alertQuery)
So(err2, ShouldBeNil)
So(alertQuery.Result, ShouldHaveLength, 0)
})
Convey("Alerts with same dashboard id and panel id should update", func() {
@@ -100,7 +134,7 @@ func TestAlertingDataAccess(t *testing.T) {
})
Convey("Alerts should be updated", func() {
query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1}
query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&query)
So(err2, ShouldBeNil)
@@ -149,7 +183,7 @@ func TestAlertingDataAccess(t *testing.T) {
Convey("Should save 3 dashboards", func() {
So(err, ShouldBeNil)
queryForDashboard := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1}
queryForDashboard := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&queryForDashboard)
So(err2, ShouldBeNil)
@@ -163,7 +197,7 @@ func TestAlertingDataAccess(t *testing.T) {
err = SaveAlerts(&cmd)
Convey("should delete the missing alert", func() {
query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1}
query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&query)
So(err2, ShouldBeNil)
So(len(query.Result), ShouldEqual, 2)
@@ -198,7 +232,7 @@ func TestAlertingDataAccess(t *testing.T) {
So(err, ShouldBeNil)
Convey("Alerts should be removed", func() {
query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1}
query := m.GetAlertsQuery{DashboardId: testDash.Id, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_ADMIN}}
err2 := HandleAlertsQuery(&query)
So(testDash.Id, ShouldEqual, 1)
@@ -208,3 +242,90 @@ func TestAlertingDataAccess(t *testing.T) {
})
})
}
func TestPausingAlerts(t *testing.T) {
mockTimeNow()
defer resetTimeNow()
Convey("Given an alert", t, func() {
InitTestDB(t)
testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
alert, _ := insertTestAlert("Alerting title", "Alerting message", testDash.OrgId, testDash.Id, simplejson.New())
stateDateBeforePause := alert.NewStateDate
stateDateAfterPause := stateDateBeforePause
Convey("when paused", func() {
pauseAlert(testDash.OrgId, 1, true)
Convey("the NewStateDate should be updated", func() {
alert, _ := getAlertById(1)
stateDateAfterPause = alert.NewStateDate
So(stateDateBeforePause, ShouldHappenBefore, stateDateAfterPause)
})
})
Convey("when unpaused", func() {
pauseAlert(testDash.OrgId, 1, false)
Convey("the NewStateDate should be updated again", func() {
alert, _ := getAlertById(1)
stateDateAfterUnpause := alert.NewStateDate
So(stateDateAfterPause, ShouldHappenBefore, stateDateAfterUnpause)
})
})
})
}
func pauseAlert(orgId int64, alertId int64, pauseState bool) (int64, error) {
cmd := &m.PauseAlertCommand{
OrgId: orgId,
AlertIds: []int64{alertId},
Paused: pauseState,
}
err := PauseAlert(cmd)
So(err, ShouldBeNil)
return cmd.ResultCount, err
}
func insertTestAlert(title string, message string, orgId int64, dashId int64, settings *simplejson.Json) (*m.Alert, error) {
items := []*m.Alert{
{
PanelId: 1,
DashboardId: dashId,
OrgId: orgId,
Name: title,
Message: message,
Settings: settings,
Frequency: 1,
},
}
cmd := m.SaveAlertsCommand{
Alerts: items,
DashboardId: dashId,
OrgId: orgId,
UserId: 1,
}
err := SaveAlerts(&cmd)
return cmd.Alerts[0], err
}
func getAlertById(id int64) (*m.Alert, error) {
q := &m.GetAlertByIdQuery{
Id: id,
}
err := GetAlertById(q)
So(err, ShouldBeNil)
return q.Result, err
}
func pauseAllAlerts(pauseState bool) error {
cmd := &m.PauseAllAlertCommand{
Paused: pauseState,
}
err := PauseAllAlerts(cmd)
So(err, ShouldBeNil)
return err
}

View File

@@ -23,6 +23,7 @@ func init() {
bus.AddHandler("sql", GetDashboardsByPluginId)
bus.AddHandler("sql", GetDashboardPermissionsForUser)
bus.AddHandler("sql", GetDashboardsBySlug)
bus.AddHandler("sql", ValidateDashboardBeforeSave)
}
var generateNewUid func() string = util.GenerateShortUid
@@ -36,38 +37,35 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error {
func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
dash := cmd.GetDashboardModel()
if err := getExistingDashboardForUpdate(sess, dash, cmd); err != nil {
return err
userId := cmd.UserId
if userId == 0 {
userId = -1
}
var existingByTitleAndFolder m.Dashboard
dashWithTitleAndFolderExists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, dialect.BooleanStr(true), dash.FolderId).Get(&existingByTitleAndFolder)
if err != nil {
return err
}
if dashWithTitleAndFolderExists {
if dash.Id != existingByTitleAndFolder.Id {
if existingByTitleAndFolder.IsFolder && !cmd.IsFolder {
return m.ErrDashboardWithSameNameAsFolder
}
if !existingByTitleAndFolder.IsFolder && cmd.IsFolder {
return m.ErrDashboardFolderWithSameNameAsDashboard
}
if dash.Id > 0 {
var existing m.Dashboard
dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing)
if err != nil {
return err
}
if !dashWithIdExists {
return m.ErrDashboardNotFound
}
// check for is someone else has written in between
if dash.Version != existing.Version {
if cmd.Overwrite {
dash.Id = existingByTitleAndFolder.Id
dash.Version = existingByTitleAndFolder.Version
if dash.Uid == "" {
dash.Uid = existingByTitleAndFolder.Uid
}
dash.SetVersion(existing.Version)
} else {
return m.ErrDashboardWithSameNameInFolderExists
return m.ErrDashboardVersionMismatch
}
}
// do not allow plugin dashboard updates without overwrite flag
if existing.PluginId != "" && cmd.Overwrite == false {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
}
if dash.Uid == "" {
@@ -75,26 +73,32 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
if err != nil {
return err
}
dash.Uid = uid
dash.Data.Set("uid", uid)
dash.SetUid(uid)
}
parentVersion := dash.Version
affectedRows := int64(0)
var err error
if dash.Id == 0 {
dash.Version = 1
dash.SetVersion(1)
dash.Created = time.Now()
dash.CreatedBy = userId
dash.Updated = time.Now()
dash.UpdatedBy = userId
metrics.M_Api_Dashboard_Insert.Inc()
dash.Data.Set("version", dash.Version)
affectedRows, err = sess.Insert(dash)
} else {
dash.Version++
dash.Data.Set("version", dash.Version)
dash.SetVersion(dash.Version + 1)
if !cmd.UpdatedAt.IsZero() {
dash.Updated = cmd.UpdatedAt
} else {
dash.Updated = time.Now()
}
dash.UpdatedBy = userId
affectedRows, err = sess.MustCols("folder_id").ID(dash.Id).Update(dash)
}
@@ -145,72 +149,6 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
return err
}
func getExistingDashboardForUpdate(sess *DBSession, dash *m.Dashboard, cmd *m.SaveDashboardCommand) (err error) {
dashWithIdExists := false
var existingById m.Dashboard
if dash.Id > 0 {
dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById)
if err != nil {
return err
}
if !dashWithIdExists {
return m.ErrDashboardNotFound
}
if dash.Uid == "" {
dash.Uid = existingById.Uid
}
}
dashWithUidExists := false
var existingByUid m.Dashboard
if dash.Uid != "" {
dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid)
if err != nil {
return err
}
}
if !dashWithIdExists && !dashWithUidExists {
return nil
}
if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id {
return m.ErrDashboardWithSameUIDExists
}
existing := existingById
if !dashWithIdExists && dashWithUidExists {
dash.Id = existingByUid.Id
existing = existingByUid
}
if (existing.IsFolder && !cmd.IsFolder) ||
(!existing.IsFolder && cmd.IsFolder) {
return m.ErrDashboardTypeMismatch
}
// check for is someone else has written in between
if dash.Version != existing.Version {
if cmd.Overwrite {
dash.Version = existing.Version
} else {
return m.ErrDashboardVersionMismatch
}
}
// do not allow plugin dashboard updates without overwrite flag
if existing.PluginId != "" && cmd.Overwrite == false {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
return nil
}
func generateNewDashboardUid(sess *DBSession, orgId int64) (string, error) {
for i := 0; i < 3; i++ {
uid := generateNewUid()
@@ -238,8 +176,8 @@ func GetDashboard(query *m.GetDashboardQuery) error {
return m.ErrDashboardNotFound
}
dashboard.Data.Set("id", dashboard.Id)
dashboard.Data.Set("uid", dashboard.Uid)
dashboard.SetId(dashboard.Id)
dashboard.SetUid(dashboard.Uid)
query.Result = &dashboard
return nil
}
@@ -392,7 +330,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
}
}
if err := DeleteAlertDefinition(dashboard.Id, sess); err != nil {
if err := deleteAlertDefinition(dashboard.Id, sess); err != nil {
return nil
}
@@ -548,3 +486,128 @@ func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error {
query.Result = us
return nil
}
func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDashboardBeforeSaveCommand) (err error) {
dash := cmd.Dashboard
dashWithIdExists := false
var existingById m.Dashboard
if dash.Id > 0 {
dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existingById)
if err != nil {
return err
}
if !dashWithIdExists {
return m.ErrDashboardNotFound
}
if dash.Uid == "" {
dash.SetUid(existingById.Uid)
}
}
dashWithUidExists := false
var existingByUid m.Dashboard
if dash.Uid != "" {
dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgId, dash.Uid).Get(&existingByUid)
if err != nil {
return err
}
}
if dash.FolderId > 0 {
var existingFolder m.Dashboard
folderExists, folderErr := sess.Where("org_id=? AND id=? AND is_folder=?", dash.OrgId, dash.FolderId, dialect.BooleanStr(true)).Get(&existingFolder)
if folderErr != nil {
return folderErr
}
if !folderExists {
return m.ErrDashboardFolderNotFound
}
}
if !dashWithIdExists && !dashWithUidExists {
return nil
}
if dashWithIdExists && dashWithUidExists && existingById.Id != existingByUid.Id {
return m.ErrDashboardWithSameUIDExists
}
existing := existingById
if !dashWithIdExists && dashWithUidExists {
dash.SetId(existingByUid.Id)
dash.SetUid(existingByUid.Uid)
existing = existingByUid
}
if (existing.IsFolder && !dash.IsFolder) ||
(!existing.IsFolder && dash.IsFolder) {
return m.ErrDashboardTypeMismatch
}
// check for is someone else has written in between
if dash.Version != existing.Version {
if cmd.Overwrite {
dash.SetVersion(existing.Version)
} else {
return m.ErrDashboardVersionMismatch
}
}
// do not allow plugin dashboard updates without overwrite flag
if existing.PluginId != "" && cmd.Overwrite == false {
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
}
return nil
}
func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashboardBeforeSaveCommand) error {
dash := cmd.Dashboard
var existing m.Dashboard
exists, err := sess.Where("org_id=? AND slug=? AND (is_folder=? OR folder_id=?)", dash.OrgId, dash.Slug, dialect.BooleanStr(true), dash.FolderId).Get(&existing)
if err != nil {
return err
}
if exists && dash.Id != existing.Id {
if existing.IsFolder && !dash.IsFolder {
return m.ErrDashboardWithSameNameAsFolder
}
if !existing.IsFolder && dash.IsFolder {
return m.ErrDashboardFolderWithSameNameAsDashboard
}
if cmd.Overwrite {
dash.SetId(existing.Id)
dash.SetUid(existing.Uid)
dash.SetVersion(existing.Version)
} else {
return m.ErrDashboardWithSameNameInFolderExists
}
}
return nil
}
func ValidateDashboardBeforeSave(cmd *m.ValidateDashboardBeforeSaveCommand) (err error) {
return inTransaction(func(sess *DBSession) error {
if err = getExistingDashboardByIdOrUidForUpdate(sess, cmd); err != nil {
return err
}
if err = getExistingDashboardByTitleAndFolder(sess, cmd); err != nil {
return err
}
return nil
})
}

View File

@@ -3,7 +3,6 @@ package sqlstore
import (
"testing"
"github.com/go-xorm/xorm"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
@@ -11,10 +10,8 @@ import (
)
func TestDashboardFolderDataAccess(t *testing.T) {
var x *xorm.Engine
Convey("Testing DB", t, func() {
x = InitTestDB(t)
InitTestDB(t)
Convey("Given one dashboard folder with two dashboards and one dashboard in the root folder", func() {
folder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
@@ -49,6 +46,7 @@ func TestDashboardFolderDataAccess(t *testing.T) {
OrgId: 1, DashboardIds: []int64{folder.Id, dashInRoot.Id},
}
err := SearchDashboards(query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].Id, ShouldEqual, dashInRoot.Id)

View File

@@ -26,8 +26,8 @@ func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error
}
cmd.Result = cmd.DashboardCmd.Result
if cmd.DashboardProvisioning.Updated.IsZero() {
cmd.DashboardProvisioning.Updated = cmd.Result.Updated
if cmd.DashboardProvisioning.Updated == 0 {
cmd.DashboardProvisioning.Updated = cmd.Result.Updated.Unix()
}
return saveProvionedData(sess, cmd.DashboardProvisioning, cmd.Result)

View File

@@ -31,7 +31,7 @@ func TestDashboardProvisioningTest(t *testing.T) {
DashboardProvisioning: &models.DashboardProvisioning{
Name: "default",
ExternalId: "/var/grafana.json",
Updated: now,
Updated: now.Unix(),
},
}
@@ -48,7 +48,7 @@ func TestDashboardProvisioningTest(t *testing.T) {
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].DashboardId, ShouldEqual, dashId)
So(query.Result[0].Updated.Unix(), ShouldEqual, now.Unix())
So(query.Result[0].Updated, ShouldEqual, now.Unix())
})
})
})

View File

@@ -0,0 +1,932 @@
package sqlstore
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestIntegratedDashboardService(t *testing.T) {
Convey("Dashboard service integration tests", t, func() {
InitTestDB(t)
var testOrgId int64 = 1
Convey("Given saved folders and dashboards in organization A", func() {
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
return nil
})
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
return nil
})
savedFolder := saveTestFolder("Saved folder", testOrgId)
savedDashInFolder := saveTestDashboard("Saved dash in folder", testOrgId, savedFolder.Id)
saveTestDashboard("Other saved dash in folder", testOrgId, savedFolder.Id)
savedDashInGeneralFolder := saveTestDashboard("Saved dashboard in general folder", testOrgId, 0)
otherSavedFolder := saveTestFolder("Other saved folder", testOrgId)
Convey("Should return dashboard model", func() {
So(savedFolder.Title, ShouldEqual, "Saved folder")
So(savedFolder.Slug, ShouldEqual, "saved-folder")
So(savedFolder.Id, ShouldNotEqual, 0)
So(savedFolder.IsFolder, ShouldBeTrue)
So(savedFolder.FolderId, ShouldEqual, 0)
So(len(savedFolder.Uid), ShouldBeGreaterThan, 0)
So(savedDashInFolder.Title, ShouldEqual, "Saved dash in folder")
So(savedDashInFolder.Slug, ShouldEqual, "saved-dash-in-folder")
So(savedDashInFolder.Id, ShouldNotEqual, 0)
So(savedDashInFolder.IsFolder, ShouldBeFalse)
So(savedDashInFolder.FolderId, ShouldEqual, savedFolder.Id)
So(len(savedDashInFolder.Uid), ShouldBeGreaterThan, 0)
})
// Basic validation tests
Convey("When saving a dashboard with non-existing id", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": float64(123412321),
"title": "Expect error",
}),
}
err := callSaveWithError(cmd)
Convey("It should result in not found error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardNotFound)
})
})
// Given other organization
Convey("Given organization B", func() {
var otherOrgId int64 = 2
Convey("When saving a dashboard with id that are saved in organization A", func() {
cmd := models.SaveDashboardCommand{
OrgId: otherOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInFolder.Id,
"title": "Expect error",
}),
Overwrite: false,
}
err := callSaveWithError(cmd)
Convey("It should result in not found error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardNotFound)
})
})
permissionScenario("Given user has permission to save", true, func(sc *dashboardPermissionScenarioContext) {
Convey("When saving a dashboard with uid that are saved in organization A", func() {
var otherOrgId int64 = 2
cmd := models.SaveDashboardCommand{
OrgId: otherOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "Dash with existing uid in other org",
}),
Overwrite: false,
}
res := callSaveWithResult(cmd)
Convey("It should create dashboard in other organization", func() {
So(res, ShouldNotBeNil)
query := models.GetDashboardQuery{OrgId: otherOrgId, Uid: savedDashInFolder.Uid}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldNotEqual, savedDashInFolder.Id)
So(query.Result.Id, ShouldEqual, res.Id)
So(query.Result.OrgId, ShouldEqual, otherOrgId)
So(query.Result.Uid, ShouldEqual, savedDashInFolder.Uid)
})
})
})
})
// Given user has no permission to save
permissionScenario("Given user has no permission to save", false, func(sc *dashboardPermissionScenarioContext) {
Convey("When trying to create a new dashboard in the General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, 0)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When trying to create a new dashboard in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
FolderId: otherSavedFolder.Id,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and rsult in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, otherSavedFolder.Id)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When trying to update a dashboard by existing id in the General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInGeneralFolder.Id,
"title": "Dash",
}),
FolderId: savedDashInGeneralFolder.FolderId,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedDashInGeneralFolder.Id)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
Convey("When trying to update a dashboard by existing id in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInFolder.Id,
"title": "Dash",
}),
FolderId: savedDashInFolder.FolderId,
UserId: 10000,
Overwrite: true,
}
err := callSaveWithError(cmd)
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedDashInFolder.Id)
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
})
})
})
// Given user has permission to save
permissionScenario("Given user has permission to save", true, func(sc *dashboardPermissionScenarioContext) {
Convey("and overwrite flag is set to false", func() {
shouldOverwrite := false
Convey("When creating a dashboard in General folder with same name as dashboard in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInFolder.Title,
}),
FolderId: 0,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should create a new dashboard", func() {
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, res.Id)
So(query.Result.FolderId, ShouldEqual, 0)
})
})
Convey("When creating a dashboard in other folder with same name as dashboard in General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInGeneralFolder.Title,
}),
FolderId: savedFolder.Id,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should create a new dashboard", func() {
So(res.Id, ShouldNotEqual, savedDashInGeneralFolder.Id)
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.FolderId, ShouldEqual, savedFolder.Id)
})
})
Convey("When creating a folder with same name as dashboard in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInFolder.Title,
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should create a new folder", func() {
So(res.Id, ShouldNotEqual, savedDashInGeneralFolder.Id)
So(res.IsFolder, ShouldBeTrue)
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.FolderId, ShouldEqual, 0)
So(query.Result.IsFolder, ShouldBeTrue)
})
})
Convey("When saving a dashboard without id and uid and unique title in folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash without id and uid",
}),
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should create a new dashboard", func() {
So(res.Id, ShouldBeGreaterThan, 0)
So(len(res.Uid), ShouldBeGreaterThan, 0)
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, res.Id)
So(query.Result.Uid, ShouldEqual, res.Uid)
})
})
Convey("When saving a dashboard when dashboard id is zero ", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": 0,
"title": "Dash with zero id",
}),
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should create a new dashboard", func() {
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, res.Id)
})
})
Convey("When saving a dashboard in non-existing folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Expect error",
}),
FolderId: 123412321,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in folder not found error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardFolderNotFound)
})
})
Convey("When updating an existing dashboard by id without current version", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInGeneralFolder.Id,
"title": "test dash 23",
}),
FolderId: savedFolder.Id,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in version mismatch error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardVersionMismatch)
})
})
Convey("When updating an existing dashboard by id with current version", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInGeneralFolder.Id,
"title": "Updated title",
"version": savedDashInGeneralFolder.Version,
}),
FolderId: savedFolder.Id,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should update dashboard", func() {
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInGeneralFolder.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Title, ShouldEqual, "Updated title")
So(query.Result.FolderId, ShouldEqual, savedFolder.Id)
So(query.Result.Version, ShouldBeGreaterThan, savedDashInGeneralFolder.Version)
})
})
Convey("When updating an existing dashboard by uid without current version", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "test dash 23",
}),
FolderId: 0,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in version mismatch error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardVersionMismatch)
})
})
Convey("When updating an existing dashboard by uid with current version", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "Updated title",
"version": savedDashInFolder.Version,
}),
FolderId: 0,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should update dashboard", func() {
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInFolder.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Title, ShouldEqual, "Updated title")
So(query.Result.FolderId, ShouldEqual, 0)
So(query.Result.Version, ShouldBeGreaterThan, savedDashInFolder.Version)
})
})
Convey("When creating a dashboard with same name as dashboard in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInFolder.Title,
}),
FolderId: savedDashInFolder.FolderId,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in dashboard with same name in folder error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardWithSameNameInFolderExists)
})
})
Convey("When creating a dashboard with same name as dashboard in General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInGeneralFolder.Title,
}),
FolderId: savedDashInGeneralFolder.FolderId,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in dashboard with same name in folder error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardWithSameNameInFolderExists)
})
})
Convey("When creating a folder with same name as existing folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedFolder.Title,
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in dashboard with same name in folder error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardWithSameNameInFolderExists)
})
})
})
Convey("and overwrite flag is set to true", func() {
shouldOverwrite := true
Convey("When updating an existing dashboard by id without current version", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInGeneralFolder.Id,
"title": "Updated title",
}),
FolderId: savedFolder.Id,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should update dashboard", func() {
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInGeneralFolder.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Title, ShouldEqual, "Updated title")
So(query.Result.FolderId, ShouldEqual, savedFolder.Id)
So(query.Result.Version, ShouldBeGreaterThan, savedDashInGeneralFolder.Version)
})
})
Convey("When updating an existing dashboard by uid without current version", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "Updated title",
}),
FolderId: 0,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
So(res, ShouldNotBeNil)
Convey("It should update dashboard", func() {
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInFolder.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Title, ShouldEqual, "Updated title")
So(query.Result.FolderId, ShouldEqual, 0)
So(query.Result.Version, ShouldBeGreaterThan, savedDashInFolder.Version)
})
})
Convey("When updating uid for existing dashboard using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInFolder.Id,
"uid": "new-uid",
"title": savedDashInFolder.Title,
}),
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
Convey("It should update dashboard", func() {
So(res, ShouldNotBeNil)
So(res.Id, ShouldEqual, savedDashInFolder.Id)
So(res.Uid, ShouldEqual, "new-uid")
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: savedDashInFolder.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Uid, ShouldEqual, "new-uid")
So(query.Result.Version, ShouldBeGreaterThan, savedDashInFolder.Version)
})
})
Convey("When updating uid to an existing uid for existing dashboard using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInFolder.Id,
"uid": savedDashInGeneralFolder.Uid,
"title": savedDashInFolder.Title,
}),
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in same uid exists error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardWithSameUIDExists)
})
})
Convey("When creating a dashboard with same name as dashboard in other folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInFolder.Title,
}),
FolderId: savedDashInFolder.FolderId,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
Convey("It should overwrite existing dashboard", func() {
So(res, ShouldNotBeNil)
So(res.Id, ShouldEqual, savedDashInFolder.Id)
So(res.Uid, ShouldEqual, savedDashInFolder.Uid)
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, res.Id)
So(query.Result.Uid, ShouldEqual, res.Uid)
})
})
Convey("When creating a dashboard with same name as dashboard in General folder", func() {
cmd := models.SaveDashboardCommand{
OrgId: testOrgId,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": savedDashInGeneralFolder.Title,
}),
FolderId: savedDashInGeneralFolder.FolderId,
Overwrite: shouldOverwrite,
}
res := callSaveWithResult(cmd)
Convey("It should overwrite existing dashboard", func() {
So(res, ShouldNotBeNil)
So(res.Id, ShouldEqual, savedDashInGeneralFolder.Id)
So(res.Uid, ShouldEqual, savedDashInGeneralFolder.Uid)
query := models.GetDashboardQuery{OrgId: cmd.OrgId, Id: res.Id}
err := bus.Dispatch(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, res.Id)
So(query.Result.Uid, ShouldEqual, res.Uid)
})
})
Convey("When trying to update existing folder to a dashboard using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedFolder.Id,
"title": "new title",
}),
IsFolder: false,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in type mismatch error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardTypeMismatch)
})
})
Convey("When trying to update existing dashboard to a folder using id", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDashInFolder.Id,
"title": "new folder title",
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in type mismatch error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardTypeMismatch)
})
})
Convey("When trying to update existing folder to a dashboard using uid", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedFolder.Uid,
"title": "new title",
}),
IsFolder: false,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in type mismatch error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardTypeMismatch)
})
})
Convey("When trying to update existing dashboard to a folder using uid", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDashInFolder.Uid,
"title": "new folder title",
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in type mismatch error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardTypeMismatch)
})
})
Convey("When trying to update existing folder to a dashboard using title", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": savedFolder.Title,
}),
IsFolder: false,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in dashboard with same name as folder error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardWithSameNameAsFolder)
})
})
Convey("When trying to update existing dashboard to a folder using title", func() {
cmd := models.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": savedDashInGeneralFolder.Title,
}),
IsFolder: true,
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd)
Convey("It should result in folder with same name as dashboard error", func() {
So(err, ShouldNotBeNil)
So(err, ShouldEqual, models.ErrDashboardFolderWithSameNameAsDashboard)
})
})
})
})
})
})
}
type scenarioContext struct {
dashboardGuardianMock *guardian.FakeDashboardGuardian
}
type scenarioFunc func(c *scenarioContext)
func dashboardGuardianScenario(desc string, mock *guardian.FakeDashboardGuardian, fn scenarioFunc) {
Convey(desc, func() {
origNewDashboardGuardian := guardian.New
guardian.MockDashboardGuardian(mock)
sc := &scenarioContext{
dashboardGuardianMock: mock,
}
defer func() {
guardian.New = origNewDashboardGuardian
}()
fn(sc)
})
}
type dashboardPermissionScenarioContext struct {
dashboardGuardianMock *guardian.FakeDashboardGuardian
}
type dashboardPermissionScenarioFunc func(sc *dashboardPermissionScenarioContext)
func dashboardPermissionScenario(desc string, mock *guardian.FakeDashboardGuardian, fn dashboardPermissionScenarioFunc) {
Convey(desc, func() {
origNewDashboardGuardian := guardian.New
guardian.MockDashboardGuardian(mock)
sc := &dashboardPermissionScenarioContext{
dashboardGuardianMock: mock,
}
defer func() {
guardian.New = origNewDashboardGuardian
}()
fn(sc)
})
}
func permissionScenario(desc string, canSave bool, fn dashboardPermissionScenarioFunc) {
mock := &guardian.FakeDashboardGuardian{
CanSaveValue: canSave,
}
dashboardPermissionScenario(desc, mock, fn)
}
func callSaveWithResult(cmd models.SaveDashboardCommand) *models.Dashboard {
dto := toSaveDashboardDto(cmd)
res, _ := dashboards.NewService().SaveDashboard(&dto)
return res
}
func callSaveWithError(cmd models.SaveDashboardCommand) error {
dto := toSaveDashboardDto(cmd)
_, err := dashboards.NewService().SaveDashboard(&dto)
return err
}
func dashboardServiceScenario(desc string, mock *guardian.FakeDashboardGuardian, fn scenarioFunc) {
Convey(desc, func() {
origNewDashboardGuardian := guardian.New
guardian.MockDashboardGuardian(mock)
sc := &scenarioContext{
dashboardGuardianMock: mock,
}
defer func() {
guardian.New = origNewDashboardGuardian
}()
fn(sc)
})
}
func saveTestDashboard(title string, orgId int64, folderId int64) *models.Dashboard {
cmd := models.SaveDashboardCommand{
OrgId: orgId,
FolderId: folderId,
IsFolder: false,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
}),
}
dto := dashboards.SaveDashboardDTO{
OrgId: orgId,
Dashboard: cmd.GetDashboardModel(),
User: &models.SignedInUser{
UserId: 1,
OrgRole: models.ROLE_ADMIN,
},
}
res, err := dashboards.NewService().SaveDashboard(&dto)
So(err, ShouldBeNil)
return res
}
func saveTestFolder(title string, orgId int64) *models.Dashboard {
cmd := models.SaveDashboardCommand{
OrgId: orgId,
FolderId: 0,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
}),
}
dto := dashboards.SaveDashboardDTO{
OrgId: orgId,
Dashboard: cmd.GetDashboardModel(),
User: &models.SignedInUser{
UserId: 1,
OrgRole: models.ROLE_ADMIN,
},
}
res, err := dashboards.NewService().SaveDashboard(&dto)
So(err, ShouldBeNil)
return res
}
func toSaveDashboardDto(cmd models.SaveDashboardCommand) dashboards.SaveDashboardDTO {
dash := (&cmd).GetDashboardModel()
return dashboards.SaveDashboardDTO{
Dashboard: dash,
Message: cmd.Message,
OrgId: cmd.OrgId,
User: &models.SignedInUser{UserId: cmd.UserId},
Overwrite: cmd.Overwrite,
}
}

View File

@@ -16,20 +16,23 @@ func init() {
bus.AddHandler("sql", DeleteExpiredSnapshots)
}
// DeleteExpiredSnapshots removes snapshots with old expiry dates.
// SnapShotRemoveExpired is deprecated and should be removed in the future.
// Snapshot expiry is decided by the user when they share the snapshot.
func DeleteExpiredSnapshots(cmd *m.DeleteExpiredSnapshotsCommand) error {
return inTransaction(func(sess *DBSession) error {
var expiredCount int64 = 0
if setting.SnapShotRemoveExpired {
deleteExpiredSql := "DELETE FROM dashboard_snapshot WHERE expires < ?"
expiredResponse, err := x.Exec(deleteExpiredSql, time.Now)
if err != nil {
return err
}
expiredCount, _ = expiredResponse.RowsAffected()
if !setting.SnapShotRemoveExpired {
sqlog.Warn("[Deprecated] The snapshot_remove_expired setting is outdated. Please remove from your config.")
return nil
}
sqlog.Debug("Deleted old/expired snaphots", "expired", expiredCount)
deleteExpiredSql := "DELETE FROM dashboard_snapshot WHERE expires < ?"
expiredResponse, err := sess.Exec(deleteExpiredSql, time.Now())
if err != nil {
return err
}
cmd.DeletedRows, _ = expiredResponse.RowsAffected()
return nil
})
}
@@ -72,7 +75,7 @@ func DeleteDashboardSnapshot(cmd *m.DeleteDashboardSnapshotCommand) error {
}
func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
snapshot := m.DashboardSnapshot{Key: query.Key}
snapshot := m.DashboardSnapshot{Key: query.Key, DeleteKey: query.DeleteKey}
has, err := x.Get(&snapshot)
if err != nil {
@@ -85,6 +88,8 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
return nil
}
// SearchDashboardSnapshots returns a list of all snapshots for admins
// for other roles, it returns snapshots created by the user
func SearchDashboardSnapshots(query *m.GetDashboardSnapshotsQuery) error {
var snapshots = make(m.DashboardSnapshotsList, 0)
@@ -95,7 +100,16 @@ func SearchDashboardSnapshots(query *m.GetDashboardSnapshotsQuery) error {
sess.Where("name LIKE ?", query.Name)
}
sess.Where("org_id = ?", query.OrgId)
// admins can see all snapshots, everyone else can only see their own snapshots
if query.SignedInUser.OrgRole == m.ROLE_ADMIN {
sess.Where("org_id = ?", query.OrgId)
} else if !query.SignedInUser.IsAnonymous {
sess.Where("org_id = ? AND user_id = ?", query.OrgId, query.SignedInUser.UserId)
} else {
query.Result = snapshots
return nil
}
err := sess.Find(&snapshots)
query.Result = snapshots
return err

View File

@@ -2,11 +2,14 @@ package sqlstore
import (
"testing"
"time"
"github.com/go-xorm/xorm"
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
func TestDashboardSnapshotDBAccess(t *testing.T) {
@@ -14,17 +17,19 @@ func TestDashboardSnapshotDBAccess(t *testing.T) {
Convey("Testing DashboardSnapshot data access", t, func() {
InitTestDB(t)
Convey("Given saved snaphot", func() {
Convey("Given saved snapshot", func() {
cmd := m.CreateDashboardSnapshotCommand{
Key: "hej",
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"hello": "mupp",
}),
UserId: 1000,
OrgId: 1,
}
err := CreateDashboardSnapshot(&cmd)
So(err, ShouldBeNil)
Convey("Should be able to get snaphot by key", func() {
Convey("Should be able to get snapshot by key", func() {
query := m.GetDashboardSnapshotQuery{Key: "hej"}
err = GetDashboardSnapshot(&query)
So(err, ShouldBeNil)
@@ -33,6 +38,135 @@ func TestDashboardSnapshotDBAccess(t *testing.T) {
So(query.Result.Dashboard.Get("hello").MustString(), ShouldEqual, "mupp")
})
Convey("And the user has the admin role", func() {
Convey("Should return all the snapshots", func() {
query := m.GetDashboardSnapshotsQuery{
OrgId: 1,
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN},
}
err := SearchDashboardSnapshots(&query)
So(err, ShouldBeNil)
So(query.Result, ShouldNotBeNil)
So(len(query.Result), ShouldEqual, 1)
})
})
Convey("And the user has the editor role and has created a snapshot", func() {
Convey("Should return all the snapshots", func() {
query := m.GetDashboardSnapshotsQuery{
OrgId: 1,
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR, UserId: 1000},
}
err := SearchDashboardSnapshots(&query)
So(err, ShouldBeNil)
So(query.Result, ShouldNotBeNil)
So(len(query.Result), ShouldEqual, 1)
})
})
Convey("And the user has the editor role and has not created any snapshot", func() {
Convey("Should not return any snapshots", func() {
query := m.GetDashboardSnapshotsQuery{
OrgId: 1,
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR, UserId: 2},
}
err := SearchDashboardSnapshots(&query)
So(err, ShouldBeNil)
So(query.Result, ShouldNotBeNil)
So(len(query.Result), ShouldEqual, 0)
})
})
Convey("And the user is anonymous", func() {
cmd := m.CreateDashboardSnapshotCommand{
Key: "strangesnapshotwithuserid0",
DeleteKey: "adeletekey",
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"hello": "mupp",
}),
UserId: 0,
OrgId: 1,
}
err := CreateDashboardSnapshot(&cmd)
So(err, ShouldBeNil)
Convey("Should not return any snapshots", func() {
query := m.GetDashboardSnapshotsQuery{
OrgId: 1,
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR, IsAnonymous: true, UserId: 0},
}
err := SearchDashboardSnapshots(&query)
So(err, ShouldBeNil)
So(query.Result, ShouldNotBeNil)
So(len(query.Result), ShouldEqual, 0)
})
})
})
})
}
func TestDeleteExpiredSnapshots(t *testing.T) {
Convey("Testing dashboard snapshots clean up", t, func() {
x := InitTestDB(t)
setting.SnapShotRemoveExpired = true
notExpiredsnapshot := createTestSnapshot(x, "key1", 1000)
createTestSnapshot(x, "key2", -1000)
createTestSnapshot(x, "key3", -1000)
Convey("Clean up old dashboard snapshots", func() {
err := DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{})
So(err, ShouldBeNil)
query := m.GetDashboardSnapshotsQuery{
OrgId: 1,
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN},
}
err = SearchDashboardSnapshots(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 1)
So(query.Result[0].Key, ShouldEqual, notExpiredsnapshot.Key)
})
Convey("Don't delete anything if there are no expired snapshots", func() {
err := DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{})
So(err, ShouldBeNil)
query := m.GetDashboardSnapshotsQuery{
OrgId: 1,
SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN},
}
SearchDashboardSnapshots(&query)
So(len(query.Result), ShouldEqual, 1)
})
})
}
func createTestSnapshot(x *xorm.Engine, key string, expires int64) *m.DashboardSnapshot {
cmd := m.CreateDashboardSnapshotCommand{
Key: key,
DeleteKey: "delete" + key,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"hello": "mupp",
}),
UserId: 1000,
OrgId: 1,
Expires: expires,
}
err := CreateDashboardSnapshot(&cmd)
So(err, ShouldBeNil)
// Set expiry date manually - to be able to create expired snapshots
expireDate := time.Now().Add(time.Second * time.Duration(expires))
_, err = x.Exec("update dashboard_snapshot set expires = ? where "+dialect.Quote("key")+" = ?", expireDate, key)
So(err, ShouldBeNil)
return cmd.Result
}

View File

@@ -3,8 +3,8 @@ package sqlstore
import (
"fmt"
"testing"
"time"
"github.com/go-xorm/xorm"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/search"
@@ -14,10 +14,8 @@ import (
)
func TestDashboardDataAccess(t *testing.T) {
var x *xorm.Engine
Convey("Testing DB", t, func() {
x = InitTestDB(t)
InitTestDB(t)
Convey("Given saved dashboard", func() {
savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")
@@ -100,324 +98,6 @@ func TestDashboardDataAccess(t *testing.T) {
So(err, ShouldBeNil)
})
Convey("Should return not found error if no dashboard is found for update", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Overwrite: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": float64(123412321),
"title": "Expect error",
"tags": []interface{}{},
}),
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardNotFound)
})
Convey("Should not be able to overwrite dashboard in another org", func() {
query := m.GetDashboardQuery{Slug: "test-dash-23", OrgId: 1}
GetDashboard(&query)
cmd := m.SaveDashboardCommand{
OrgId: 2,
Overwrite: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": float64(query.Result.Id),
"title": "Expect error",
"tags": []interface{}{},
}),
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardNotFound)
})
Convey("Should be able to save dashboards with same name in different folders", func() {
firstSaveCmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dash folder and title",
"tags": []interface{}{},
"uid": "randomHash",
}),
FolderId: 3,
}
err := SaveDashboard(&firstSaveCmd)
So(err, ShouldBeNil)
secondSaveCmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dash folder and title",
"tags": []interface{}{},
"uid": "moreRandomHash",
}),
FolderId: 1,
}
err = SaveDashboard(&secondSaveCmd)
So(err, ShouldBeNil)
So(firstSaveCmd.Result.Id, ShouldNotEqual, secondSaveCmd.Result.Id)
})
Convey("Should be able to overwrite dashboard in same folder using title", func() {
insertTestDashboard("Dash", 1, 0, false, "prod", "webapp")
folder := insertTestDashboard("Folder", 1, 0, true, "prod", "webapp")
dashInFolder := insertTestDashboard("Dash", 1, folder.Id, false, "prod", "webapp")
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
FolderId: folder.Id,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
So(cmd.Result.Id, ShouldEqual, dashInFolder.Id)
So(cmd.Result.Uid, ShouldEqual, dashInFolder.Uid)
})
Convey("Should be able to overwrite dashboard in General folder using title", func() {
dashInGeneral := insertTestDashboard("Dash", 1, 0, false, "prod", "webapp")
folder := insertTestDashboard("Folder", 1, 0, true, "prod", "webapp")
insertTestDashboard("Dash", 1, folder.Id, false, "prod", "webapp")
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "Dash",
}),
FolderId: 0,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
So(cmd.Result.Id, ShouldEqual, dashInGeneral.Id)
So(cmd.Result.Uid, ShouldEqual, dashInGeneral.Uid)
})
Convey("Should not be able to overwrite folder with dashboard in general folder using title", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": savedFolder.Title,
}),
FolderId: 0,
IsFolder: false,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardWithSameNameAsFolder)
})
Convey("Should not be able to overwrite folder with dashboard in folder using title", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": savedFolder.Title,
}),
FolderId: savedFolder.Id,
IsFolder: false,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardWithSameNameAsFolder)
})
Convey("Should not be able to overwrite folder with dashboard using id", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedFolder.Id,
"title": "new title",
}),
IsFolder: false,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
})
Convey("Should not be able to overwrite dashboard with folder using id", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": savedDash.Id,
"title": "new folder title",
}),
IsFolder: true,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
})
Convey("Should not be able to overwrite folder with dashboard using uid", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedFolder.Uid,
"title": "new title",
}),
IsFolder: false,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
})
Convey("Should not be able to overwrite dashboard with folder using uid", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDash.Uid,
"title": "new folder title",
}),
IsFolder: true,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardTypeMismatch)
})
Convey("Should not be able to save dashboard with same name in the same folder without overwrite", func() {
firstSaveCmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dash folder and title",
"tags": []interface{}{},
"uid": "randomHash",
}),
FolderId: 3,
}
err := SaveDashboard(&firstSaveCmd)
So(err, ShouldBeNil)
secondSaveCmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test dash folder and title",
"tags": []interface{}{},
"uid": "moreRandomHash",
}),
FolderId: 3,
}
err = SaveDashboard(&secondSaveCmd)
So(err, ShouldEqual, m.ErrDashboardWithSameNameInFolderExists)
})
Convey("Should be able to save and update dashboard using same uid", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"uid": "dsfalkjngailuedt",
"title": "test dash 23",
}),
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
err = SaveDashboard(&cmd)
So(err, ShouldBeNil)
})
Convey("Should be able to update dashboard using uid", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDash.Uid,
"title": "new title",
}),
FolderId: 0,
Overwrite: true,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
Convey("Should be able to get updated dashboard by uid", func() {
query := m.GetDashboardQuery{
Uid: savedDash.Uid,
OrgId: 1,
}
err := GetDashboard(&query)
So(err, ShouldBeNil)
So(query.Result.Id, ShouldEqual, savedDash.Id)
So(query.Result.Title, ShouldEqual, "new title")
So(query.Result.FolderId, ShouldEqual, 0)
})
})
Convey("Should be able to update dashboard with the same title and folder id", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": "randomHash",
"title": "folderId",
"style": "light",
"tags": []interface{}{},
}),
FolderId: 2,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
So(cmd.Result.FolderId, ShouldEqual, 2)
cmd = m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": cmd.Result.Id,
"uid": "randomHash",
"title": "folderId",
"style": "dark",
"version": cmd.Result.Version,
"tags": []interface{}{},
}),
FolderId: 2,
}
err = SaveDashboard(&cmd)
So(err, ShouldBeNil)
})
Convey("Should be able to update using uid without id and overwrite", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"uid": savedDash.Uid,
"title": "folderId",
"version": savedDash.Version,
"tags": []interface{}{},
}),
FolderId: savedDash.FolderId,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
})
Convey("Should retry generation of uid once if it fails.", func() {
timesCalled := 0
generateNewUid = func() string {
@@ -442,6 +122,24 @@ func TestDashboardDataAccess(t *testing.T) {
generateNewUid = util.GenerateShortUid
})
Convey("Should be able to create dashboard", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"title": "folderId",
"tags": []interface{}{},
}),
UserId: 100,
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
So(cmd.Result.CreatedBy, ShouldEqual, 100)
So(cmd.Result.Created.IsZero(), ShouldBeFalse)
So(cmd.Result.UpdatedBy, ShouldEqual, 100)
So(cmd.Result.Updated.IsZero(), ShouldBeFalse)
})
Convey("Should be able to update dashboard by id and remove folderId", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
@@ -452,6 +150,7 @@ func TestDashboardDataAccess(t *testing.T) {
}),
Overwrite: true,
FolderId: 2,
UserId: 100,
}
err := SaveDashboard(&cmd)
@@ -467,6 +166,7 @@ func TestDashboardDataAccess(t *testing.T) {
}),
FolderId: 0,
Overwrite: true,
UserId: 100,
}
err = SaveDashboard(&cmd)
@@ -480,6 +180,10 @@ func TestDashboardDataAccess(t *testing.T) {
err = GetDashboard(&query)
So(err, ShouldBeNil)
So(query.Result.FolderId, ShouldEqual, 0)
So(query.Result.CreatedBy, ShouldEqual, savedDash.CreatedBy)
So(query.Result.Created, ShouldEqual, savedDash.Created.Truncate(time.Second))
So(query.Result.UpdatedBy, ShouldEqual, 100)
So(query.Result.Updated.IsZero(), ShouldBeFalse)
})
Convey("Should be able to delete a dashboard folder and its children", func() {
@@ -499,6 +203,36 @@ func TestDashboardDataAccess(t *testing.T) {
So(len(query.Result), ShouldEqual, 0)
})
Convey("Should return error if no dashboard is found for update when dashboard id is greater than zero", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Overwrite: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": float64(123412321),
"title": "Expect error",
"tags": []interface{}{},
}),
}
err := SaveDashboard(&cmd)
So(err, ShouldEqual, m.ErrDashboardNotFound)
})
Convey("Should not return error if no dashboard is found for update when dashboard id is zero", func() {
cmd := m.SaveDashboardCommand{
OrgId: 1,
Overwrite: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": 0,
"title": "New dash",
"tags": []interface{}{},
}),
}
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
})
Convey("Should be able to get dashboard tags", func() {
query := m.GetDashboardTagsQuery{OrgId: 1}
@@ -627,6 +361,9 @@ func insertTestDashboard(title string, orgId int64, folderId int64, isFolder boo
err := SaveDashboard(&cmd)
So(err, ShouldBeNil)
cmd.Result.Data.Set("id", cmd.Result.Id)
cmd.Result.Data.Set("uid", cmd.Result.Uid)
return cmd.Result
}

View File

@@ -67,72 +67,48 @@ func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error {
return nil
}
const MAX_VERSIONS_TO_DELETE = 100
func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error {
return inTransaction(func(sess *DBSession) error {
expiredCount := int64(0)
versions := []DashboardVersionExp{}
versionsToKeep := setting.DashboardVersionsToKeep
if versionsToKeep < 1 {
versionsToKeep = 1
}
err := sess.Table("dashboard_version").
Select("dashboard_version.id, dashboard_version.version, dashboard_version.dashboard_id").
Where(`dashboard_id IN (
SELECT dashboard_id FROM dashboard_version
GROUP BY dashboard_id HAVING COUNT(dashboard_version.id) > ?
)`, versionsToKeep).
Desc("dashboard_version.dashboard_id", "dashboard_version.version").
Find(&versions)
// Idea of this query is finding version IDs to delete based on formula:
// min_version_to_keep = min_version + (versions_count - versions_to_keep)
// where version stats is processed for each dashboard. This guarantees that we keep at least versions_to_keep
// versions, but in some cases (when versions are sparse) this number may be more.
versionIdsToDeleteQuery := `SELECT id
FROM dashboard_version, (
SELECT dashboard_id, count(version) as count, min(version) as min
FROM dashboard_version
GROUP BY dashboard_id
) AS vtd
WHERE dashboard_version.dashboard_id=vtd.dashboard_id
AND version < vtd.min + vtd.count - ?`
var versionIdsToDelete []interface{}
err := sess.SQL(versionIdsToDeleteQuery, versionsToKeep).Find(&versionIdsToDelete)
if err != nil {
return err
}
// Keep last versionsToKeep versions and delete other
versionIdsToDelete := getVersionIDsToDelete(versions, versionsToKeep)
// Don't delete more than MAX_VERSIONS_TO_DELETE version per time
if len(versionIdsToDelete) > MAX_VERSIONS_TO_DELETE {
versionIdsToDelete = versionIdsToDelete[:MAX_VERSIONS_TO_DELETE]
}
if len(versionIdsToDelete) > 0 {
deleteExpiredSql := `DELETE FROM dashboard_version WHERE id IN (?` + strings.Repeat(",?", len(versionIdsToDelete)-1) + `)`
expiredResponse, err := sess.Exec(deleteExpiredSql, versionIdsToDelete...)
if err != nil {
return err
}
expiredCount, _ = expiredResponse.RowsAffected()
sqlog.Debug("Deleted old/expired dashboard versions", "expired", expiredCount)
cmd.DeletedRows, _ = expiredResponse.RowsAffected()
}
return nil
})
}
// Short version of DashboardVersion for getting expired versions
type DashboardVersionExp struct {
Id int64 `json:"id"`
DashboardId int64 `json:"dashboardId"`
Version int `json:"version"`
}
func getVersionIDsToDelete(versions []DashboardVersionExp, versionsToKeep int) []interface{} {
versionIds := make([]interface{}, 0)
if len(versions) == 0 {
return versionIds
}
currentDashboard := versions[0].DashboardId
count := 0
for _, v := range versions {
if v.DashboardId == currentDashboard {
count++
} else {
count = 1
currentDashboard = v.DashboardId
}
if count > versionsToKeep {
versionIds = append(versionIds, v.Id)
}
}
return versionIds
}

View File

@@ -12,7 +12,7 @@ import (
)
func updateTestDashboard(dashboard *m.Dashboard, data map[string]interface{}) {
data["uid"] = dashboard.Uid
data["id"] = dashboard.Id
saveCmd := m.SaveDashboardCommand{
OrgId: dashboard.OrgId,
@@ -136,10 +136,30 @@ func TestDeleteExpiredVersions(t *testing.T) {
err := DeleteExpiredVersions(&m.DeleteExpiredVersionsCommand{})
So(err, ShouldBeNil)
query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1}
query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWrite}
GetDashboardVersions(&query)
So(len(query.Result), ShouldEqual, versionsToWrite)
})
Convey("Don't delete more than MAX_VERSIONS_TO_DELETE per iteration", func() {
versionsToWriteBigNumber := MAX_VERSIONS_TO_DELETE + versionsToWrite
for i := 0; i < versionsToWriteBigNumber-versionsToWrite; i++ {
updateTestDashboard(savedDash, map[string]interface{}{
"tags": "different-tag",
})
}
err := DeleteExpiredVersions(&m.DeleteExpiredVersionsCommand{})
So(err, ShouldBeNil)
query := m.GetDashboardVersionsQuery{DashboardId: savedDash.Id, OrgId: 1, Limit: versionsToWriteBigNumber}
GetDashboardVersions(&query)
// Ensure we have at least versionsToKeep versions
So(len(query.Result), ShouldBeGreaterThanOrEqualTo, versionsToKeep)
// Ensure we haven't deleted more than MAX_VERSIONS_TO_DELETE rows
So(versionsToWriteBigNumber-len(query.Result), ShouldBeLessThanOrEqualTo, MAX_VERSIONS_TO_DELETE)
})
})
}

View File

@@ -27,6 +27,9 @@ func GetDataSourceById(query *m.GetDataSourceByIdQuery) error {
datasource := m.DataSource{OrgId: query.OrgId, Id: query.Id}
has, err := x.Get(&datasource)
if err != nil {
return err
}
if !has {
return m.ErrDataSourceNotFound

View File

@@ -1,61 +1,13 @@
package sqlstore
import (
"os"
"strings"
"testing"
"github.com/go-xorm/xorm"
. "github.com/smartystreets/goconvey/convey"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
)
var (
dbSqlite = "sqlite"
dbMySql = "mysql"
dbPostgres = "postgres"
)
func InitTestDB(t *testing.T) *xorm.Engine {
selectedDb := dbSqlite
//selectedDb := dbMySql
//selectedDb := dbPostgres
var x *xorm.Engine
var err error
// environment variable present for test db?
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
selectedDb = db
}
switch strings.ToLower(selectedDb) {
case dbMySql:
x, err = xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
case dbPostgres:
x, err = xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
default:
x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
}
// x.ShowSQL()
if err != nil {
t.Fatalf("Failed to init in memory sqllite3 db %v", err)
}
sqlutil.CleanDB(x)
if err := SetEngine(x); err != nil {
t.Fatal(err)
}
return x
}
type Test struct {
Id int64
Name string

View File

@@ -21,7 +21,7 @@ func CreateLoginAttempt(cmd *m.CreateLoginAttemptCommand) error {
loginAttempt := m.LoginAttempt{
Username: cmd.Username,
IpAddress: cmd.IpAddress,
Created: getTimeNow(),
Created: getTimeNow().Unix(),
}
if _, err := sess.Insert(&loginAttempt); err != nil {
@@ -37,8 +37,8 @@ func CreateLoginAttempt(cmd *m.CreateLoginAttemptCommand) error {
func DeleteOldLoginAttempts(cmd *m.DeleteOldLoginAttemptsCommand) error {
return inTransaction(func(sess *DBSession) error {
var maxId int64
sql := "SELECT max(id) as id FROM login_attempt WHERE created < " + dialect.DateTimeFunc("?")
result, err := sess.Query(sql, cmd.OlderThan)
sql := "SELECT max(id) as id FROM login_attempt WHERE created < ?"
result, err := sess.Query(sql, cmd.OlderThan.Unix())
if err != nil {
return err
@@ -66,7 +66,7 @@ func GetUserLoginAttemptCount(query *m.GetUserLoginAttemptCountQuery) error {
loginAttempt := new(m.LoginAttempt)
total, err := x.
Where("username = ?", query.Username).
And("created >="+dialect.DateTimeFunc("?"), query.Since).
And("created >= ?", query.Since.Unix()).
Count(loginAttempt)
if err != nil {

View File

@@ -24,3 +24,24 @@ func addTableRenameMigration(mg *Migrator, oldName string, newName string, versi
migrationId := fmt.Sprintf("Rename table %s to %s - %s", oldName, newName, versionSuffix)
mg.AddMigration(migrationId, NewRenameTableMigration(oldName, newName))
}
func addTableReplaceMigrations(mg *Migrator, from Table, to Table, migrationVersion int64, tableDataMigration map[string]string) {
fromV := version(migrationVersion - 1)
toV := version(migrationVersion)
tmpTableName := to.Name + "_tmp_qwerty"
createTable := fmt.Sprintf("create %v %v", to.Name, toV)
copyTableData := fmt.Sprintf("copy %v %v to %v", to.Name, fromV, toV)
dropTable := fmt.Sprintf("drop %v", tmpTableName)
addDropAllIndicesMigrations(mg, fromV, from)
addTableRenameMigration(mg, from.Name, tmpTableName, fromV)
mg.AddMigration(createTable, NewAddTableMigration(to))
addTableIndicesMigrations(mg, toV, to)
mg.AddMigration(copyTableData, NewCopyTableDataMigration(to.Name, tmpTableName, tableDataMigration))
mg.AddMigration(dropTable, NewDropTableMigration(tmpTableName))
}
func version(v int64) string {
return fmt.Sprintf("v%v", v)
}

View File

@@ -1,6 +1,8 @@
package migrations
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
import (
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
func addDashboardMigration(mg *Migrator) {
var dashboardV1 = Table{
@@ -181,15 +183,34 @@ func addDashboardMigration(mg *Migrator) {
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "dashboard_id", Type: DB_BigInt, Nullable: true},
{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "name", Type: DB_NVarchar, Length: 150, Nullable: false},
{Name: "external_id", Type: DB_Text, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false},
},
Indices: []*Index{},
}
mg.AddMigration("create dashboard_provisioning", NewAddTableMigration(dashboardExtrasTable))
dashboardExtrasTableV2 := Table{
Name: "dashboard_provisioning",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "dashboard_id", Type: DB_BigInt, Nullable: true},
{Name: "name", Type: DB_NVarchar, Length: 150, Nullable: false},
{Name: "external_id", Type: DB_Text, Nullable: false},
{Name: "updated", Type: DB_Int, Default: "0", Nullable: false},
},
Indices: []*Index{
{Cols: []string{"dashboard_id"}},
{Cols: []string{"dashboard_id", "name"}, Type: IndexType},
},
}
mg.AddMigration("create dashboard_provisioning", NewAddTableMigration(dashboardExtrasTable))
addTableReplaceMigrations(mg, dashboardExtrasTable, dashboardExtrasTableV2, 2, map[string]string{
"id": "id",
"dashboard_id": "dashboard_id",
"name": "name",
"external_id": "external_id",
})
}

View File

@@ -20,4 +20,23 @@ func addLoginAttemptMigrations(mg *Migrator) {
mg.AddMigration("create login attempt table", NewAddTableMigration(loginAttemptV1))
// add indices
mg.AddMigration("add index login_attempt.username", NewAddIndexMigration(loginAttemptV1, loginAttemptV1.Indices[0]))
loginAttemptV2 := Table{
Name: "login_attempt",
Columns: []*Column{
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "username", Type: DB_NVarchar, Length: 190, Nullable: false},
{Name: "ip_address", Type: DB_NVarchar, Length: 30, Nullable: false},
{Name: "created", Type: DB_Int, Default: "0", Nullable: false},
},
Indices: []*Index{
{Cols: []string{"username"}},
},
}
addTableReplaceMigrations(mg, loginAttemptV1, loginAttemptV2, 2, map[string]string{
"id": "id",
"username": "username",
"ip_address": "ip_address",
})
}

View File

@@ -14,13 +14,15 @@ import (
var indexTypes = []string{"Unknown", "INDEX", "UNIQUE INDEX"}
func TestMigrations(t *testing.T) {
//log.NewLogger(0, "console", `{"level": 0}`)
testDBs := []sqlutil.TestDB{
sqlutil.TestDB_Sqlite3,
}
for _, testDB := range testDBs {
sql := `select count(*) as count from migration_log`
r := struct {
Count int64
}{}
Convey("Initial "+testDB.DriverName+" migration", t, func() {
x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
@@ -28,30 +30,31 @@ func TestMigrations(t *testing.T) {
sqlutil.CleanDB(x)
has, err := x.SQL(sql).Get(&r)
So(err, ShouldNotBeNil)
mg := NewMigrator(x)
AddMigrations(mg)
err = mg.Start()
So(err, ShouldBeNil)
// tables, err := x.DBMetas()
// So(err, ShouldBeNil)
//
// fmt.Printf("\nDB Schema after migration: table count: %v\n", len(tables))
//
// for _, table := range tables {
// fmt.Printf("\nTable: %v \n", table.Name)
// for _, column := range table.Columns() {
// fmt.Printf("\t %v \n", column.String(x.Dialect()))
// }
//
// if len(table.Indexes) > 0 {
// fmt.Printf("\n\tIndexes:\n")
// for _, index := range table.Indexes {
// fmt.Printf("\t %v (%v) %v \n", index.Name, strings.Join(index.Cols, ","), indexTypes[index.Type])
// }
// }
// }
has, err = x.SQL(sql).Get(&r)
So(err, ShouldBeNil)
So(has, ShouldBeTrue)
expectedMigrations := mg.MigrationsCount() - 2 //we currently skip to migrations. We should rewrite skipped migrations to write in the log as well. until then we have to keep this
So(r.Count, ShouldEqual, expectedMigrations)
mg = NewMigrator(x)
AddMigrations(mg)
err = mg.Start()
So(err, ShouldBeNil)
has, err = x.SQL(sql).Get(&r)
So(err, ShouldBeNil)
So(has, ShouldBeTrue)
So(r.Count, ShouldEqual, expectedMigrations)
})
}
}

View File

@@ -35,6 +35,10 @@ func NewMigrator(engine *xorm.Engine) *Migrator {
return mg
}
func (mg *Migrator) MigrationsCount() int {
return len(mg.migrations)
}
func (mg *Migrator) AddMigration(id string, m Migration) {
m.SetId(id)
mg.migrations = append(mg.migrations, m)
@@ -121,7 +125,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
condition := m.GetCondition()
if condition != nil {
sql, args := condition.Sql(mg.dialect)
results, err := sess.Query(sql, args...)
results, err := sess.SQL(sql).Query(args...)
if err != nil || len(results) == 0 {
mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
return sess.Rollback()

View File

@@ -2,6 +2,7 @@ package sqlstore
import (
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
@@ -241,6 +242,8 @@ func TestAccountDataAccess(t *testing.T) {
func testHelperUpdateDashboardAcl(dashboardId int64, items ...m.DashboardAcl) error {
cmd := m.UpdateDashboardAclCommand{DashboardId: dashboardId}
for _, item := range items {
item.Created = time.Now()
item.Updated = time.Now()
cmd.Items = append(cmd.Items, &item)
}
return UpdateDashboardAcl(&cmd)

View File

@@ -2,6 +2,7 @@ package sqlstore
import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
@@ -98,8 +99,9 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
return inTransaction(func(sess *DBSession) error {
//Check if quota is already defined in the DB
quota := m.Quota{
Target: cmd.Target,
OrgId: cmd.OrgId,
Target: cmd.Target,
OrgId: cmd.OrgId,
Updated: time.Now(),
}
has, err := sess.Get(&quota)
if err != nil {
@@ -107,6 +109,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
}
quota.Limit = cmd.Limit
if has == false {
quota.Created = time.Now()
//No quota in the DB for this target, so create a new one.
if _, err := sess.Insert(&quota); err != nil {
return err
@@ -198,8 +201,9 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
return inTransaction(func(sess *DBSession) error {
//Check if quota is already defined in the DB
quota := m.Quota{
Target: cmd.Target,
UserId: cmd.UserId,
Target: cmd.Target,
UserId: cmd.UserId,
Updated: time.Now(),
}
has, err := sess.Get(&quota)
if err != nil {
@@ -207,6 +211,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
}
quota.Limit = cmd.Limit
if has == false {
quota.Created = time.Now()
//No quota in the DB for this target, so create a new one.
if _, err := sess.Insert(&quota); err != nil {
return err

View File

@@ -104,12 +104,12 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
})
})
Convey("Given saved user quota for org", func() {
userQoutaCmd := m.UpdateUserQuotaCmd{
userQuotaCmd := m.UpdateUserQuotaCmd{
UserId: userId,
Target: "org_user",
Limit: 10,
}
err := UpdateUserQuota(&userQoutaCmd)
err := UpdateUserQuota(&userQuotaCmd)
So(err, ShouldBeNil)
Convey("Should be able to get saved quota by user id and target", func() {

View File

@@ -12,6 +12,22 @@ type SqlBuilder struct {
params []interface{}
}
func (sb *SqlBuilder) Write(sql string, params ...interface{}) {
sb.sql.WriteString(sql)
if len(params) > 0 {
sb.params = append(sb.params, params...)
}
}
func (sb *SqlBuilder) GetSqlString() string {
return sb.sql.String()
}
func (sb *SqlBuilder) AddParams(params ...interface{}) {
sb.params = append(sb.params, params...)
}
func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, permission m.PermissionType) {
if user.OrgRole == m.ROLE_ADMIN {

View File

@@ -7,14 +7,16 @@ import (
"path"
"path/filepath"
"strings"
"testing"
"time"
"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/annotations"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
"github.com/grafana/grafana/pkg/setting"
"github.com/go-sql-driver/mysql"
@@ -22,6 +24,8 @@ import (
"github.com/go-xorm/xorm"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
_ "github.com/grafana/grafana/pkg/tsdb/mssql"
)
type DatabaseConfig struct {
@@ -32,6 +36,7 @@ type DatabaseConfig struct {
ServerCertName string
MaxOpenConn int
MaxIdleConn int
ConnMaxLifetime int
}
var (
@@ -101,7 +106,6 @@ func SetEngine(engine *xorm.Engine) (err error) {
// Init repo instances
annotations.SetRepository(&SqlAnnotationRepo{})
dashboards.SetRepository(&dashboards.DashboardRepository{})
return nil
}
@@ -157,18 +161,20 @@ func getEngine() (*xorm.Engine, error) {
engine, err := xorm.NewEngine(DbCfg.Type, cnnstr)
if err != nil {
return nil, err
} else {
engine.SetMaxOpenConns(DbCfg.MaxOpenConn)
engine.SetMaxIdleConns(DbCfg.MaxIdleConn)
debugSql := setting.Cfg.Section("database").Key("log_queries").MustBool(false)
if !debugSql {
engine.SetLogger(&xorm.DiscardLogger{})
} else {
engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm")))
engine.ShowSQL(true)
engine.ShowExecTime(true)
}
}
engine.SetMaxOpenConns(DbCfg.MaxOpenConn)
engine.SetMaxIdleConns(DbCfg.MaxIdleConn)
engine.SetConnMaxLifetime(time.Second * time.Duration(DbCfg.ConnMaxLifetime))
debugSql := setting.Cfg.Section("database").Key("log_queries").MustBool(false)
if !debugSql {
engine.SetLogger(&xorm.DiscardLogger{})
} else {
engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm")))
engine.ShowSQL(true)
engine.ShowExecTime(true)
}
return engine, nil
}
@@ -202,6 +208,7 @@ func LoadConfig() {
}
DbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0)
DbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(0)
DbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400)
if DbCfg.Type == "sqlite3" {
UseSQLite3 = true
@@ -216,3 +223,49 @@ func LoadConfig() {
DbCfg.ServerCertName = sec.Key("server_cert_name").String()
DbCfg.Path = sec.Key("path").MustString("data/grafana.db")
}
var (
dbSqlite = "sqlite"
dbMySql = "mysql"
dbPostgres = "postgres"
)
func InitTestDB(t *testing.T) *xorm.Engine {
selectedDb := dbSqlite
// selectedDb := dbMySql
// selectedDb := dbPostgres
var x *xorm.Engine
var err error
// environment variable present for test db?
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
selectedDb = db
}
switch strings.ToLower(selectedDb) {
case dbMySql:
x, err = xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
case dbPostgres:
x, err = xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
default:
x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
}
x.DatabaseTZ = time.UTC
x.TZLocation = time.UTC
// x.ShowSQL()
if err != nil {
t.Fatalf("Failed to init in memory sqllite3 db %v", err)
}
sqlutil.CleanDB(x)
if err := SetEngine(x); err != nil {
t.Fatal(err)
}
return x
}

View File

@@ -14,6 +14,7 @@ type TestDB struct {
var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:"}
var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci"}
var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
var TestDB_Mssql = TestDB{DriverName: "mssql", ConnStr: "server=localhost;port=1433;database=grafanatest;user id=grafana;password=Password!"}
func CleanDB(x *xorm.Engine) {
if x.DriverName() == "postgres" {

View File

@@ -78,11 +78,12 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error {
})
}
// DeleteTeam will delete a team, its member and any permissions connected to the team
func DeleteTeam(cmd *m.DeleteTeamCommand) error {
return inTransaction(func(sess *DBSession) error {
if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", cmd.OrgId, cmd.Id); err != nil {
if teamExists, err := teamExists(cmd.OrgId, cmd.Id, sess); err != nil {
return err
} else if len(res) != 1 {
} else if !teamExists {
return m.ErrTeamNotFound
}
@@ -102,6 +103,16 @@ func DeleteTeam(cmd *m.DeleteTeamCommand) error {
})
}
func teamExists(orgId int64, teamId int64, sess *DBSession) (bool, error) {
if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", orgId, teamId); err != nil {
return false, err
} else if len(res) != 1 {
return false, nil
}
return true, nil
}
func isTeamNameTaken(orgId int64, name string, existingId int64, sess *DBSession) (bool, error) {
var team m.Team
exists, err := sess.Where("org_id=? and name=?", orgId, name).Get(&team)
@@ -190,6 +201,7 @@ func GetTeamById(query *m.GetTeamByIdQuery) error {
return nil
}
// GetTeamsByUser is used by the Guardian when checking a users' permissions
func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
query.Result = make([]*m.Team, 0)
@@ -205,6 +217,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
return nil
}
// AddTeamMember adds a user to a team
func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
return inTransaction(func(sess *DBSession) error {
if res, err := sess.Query("SELECT 1 from team_member WHERE org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId); err != nil {
@@ -213,9 +226,9 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
return m.ErrTeamMemberAlreadyAdded
}
if res, err := sess.Query("SELECT 1 from team WHERE org_id=? and id=?", cmd.OrgId, cmd.TeamId); err != nil {
if teamExists, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
return err
} else if len(res) != 1 {
} else if !teamExists {
return m.ErrTeamNotFound
}
@@ -232,18 +245,30 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error {
})
}
// RemoveTeamMember removes a member from a team
func RemoveTeamMember(cmd *m.RemoveTeamMemberCommand) error {
return inTransaction(func(sess *DBSession) error {
if teamExists, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
return err
} else if !teamExists {
return m.ErrTeamNotFound
}
var rawSql = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?"
_, err := sess.Exec(rawSql, cmd.OrgId, cmd.TeamId, cmd.UserId)
res, err := sess.Exec(rawSql, cmd.OrgId, cmd.TeamId, cmd.UserId)
if err != nil {
return err
}
rows, err := res.RowsAffected()
if rows == 0 {
return m.ErrTeamMemberNotFound
}
return err
})
}
// GetTeamMembers return a list of members for the specified team
func GetTeamMembers(query *m.GetTeamMembersQuery) error {
query.Result = make([]*m.TeamMemberDTO, 0)
sess := x.Table("team_member")

View File

@@ -84,13 +84,16 @@ func TestTeamCommandsAndQueries(t *testing.T) {
})
Convey("Should be able to remove users from a group", func() {
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]})
So(err, ShouldBeNil)
err = RemoveTeamMember(&m.RemoveTeamMemberCommand{OrgId: testOrgId, TeamId: group1.Result.Id, UserId: userIds[0]})
So(err, ShouldBeNil)
q1 := &m.GetTeamMembersQuery{TeamId: group1.Result.Id}
err = GetTeamMembers(q1)
q2 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: group1.Result.Id}
err = GetTeamMembers(q2)
So(err, ShouldBeNil)
So(len(q1.Result), ShouldEqual, 0)
So(len(q2.Result), ShouldEqual, 0)
})
Convey("Should be able to remove a group with users and permissions", func() {

View File

@@ -315,6 +315,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
}
query.Result = m.UserProfileDTO{
Id: user.Id,
Name: user.Name,
Email: user.Email,
Login: user.Login,