Merge branch 'master' into master
This commit is contained in:
@@ -13,11 +13,7 @@ func init() {
|
||||
func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error {
|
||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
||||
|
||||
if _, err := extractor.GetAlerts(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return extractor.ValidateAlerts()
|
||||
}
|
||||
|
||||
func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
|
||||
@@ -29,15 +25,12 @@ func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error {
|
||||
|
||||
extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId)
|
||||
|
||||
if alerts, err := extractor.GetAlerts(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
saveAlerts.Alerts = alerts
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&saveAlerts); err != nil {
|
||||
alerts, err := extractor.GetAlerts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
saveAlerts.Alerts = alerts
|
||||
|
||||
return bus.Dispatch(&saveAlerts)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
defaultTypes []string = []string{"gt", "lt"}
|
||||
rangedTypes []string = []string{"within_range", "outside_range"}
|
||||
defaultTypes = []string{"gt", "lt"}
|
||||
rangedTypes = []string{"within_range", "outside_range"}
|
||||
)
|
||||
|
||||
type AlertEvaluator interface {
|
||||
@@ -20,7 +20,7 @@ type AlertEvaluator interface {
|
||||
type NoValueEvaluator struct{}
|
||||
|
||||
func (e *NoValueEvaluator) Eval(reducedValue null.Float) bool {
|
||||
return reducedValue.Valid == false
|
||||
return !reducedValue.Valid
|
||||
}
|
||||
|
||||
type ThresholdEvaluator struct {
|
||||
@@ -45,7 +45,7 @@ func newThresholdEvaluator(typ string, model *simplejson.Json) (*ThresholdEvalua
|
||||
}
|
||||
|
||||
func (e *ThresholdEvaluator) Eval(reducedValue null.Float) bool {
|
||||
if reducedValue.Valid == false {
|
||||
if !reducedValue.Valid {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
|
||||
}
|
||||
|
||||
func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
|
||||
if reducedValue.Valid == false {
|
||||
if !reducedValue.Valid {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
|
||||
reducedValue := c.Reducer.Reduce(series)
|
||||
evalMatch := c.Evaluator.Eval(reducedValue)
|
||||
|
||||
if reducedValue.Valid == false {
|
||||
if !reducedValue.Valid {
|
||||
emptySerieCount++
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,14 @@ import (
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
execQueue chan *Job
|
||||
clock clock.Clock
|
||||
type AlertingService struct {
|
||||
execQueue chan *Job
|
||||
//clock clock.Clock
|
||||
ticker *Ticker
|
||||
scheduler Scheduler
|
||||
evalHandler EvalHandler
|
||||
@@ -25,35 +27,41 @@ type Engine struct {
|
||||
resultHandler ResultHandler
|
||||
}
|
||||
|
||||
func NewEngine() *Engine {
|
||||
e := &Engine{
|
||||
ticker: NewTicker(time.Now(), time.Second*0, clock.New()),
|
||||
execQueue: make(chan *Job, 1000),
|
||||
scheduler: NewScheduler(),
|
||||
evalHandler: NewEvalHandler(),
|
||||
ruleReader: NewRuleReader(),
|
||||
log: log.New("alerting.engine"),
|
||||
resultHandler: NewResultHandler(),
|
||||
}
|
||||
func init() {
|
||||
registry.RegisterService(&AlertingService{})
|
||||
}
|
||||
|
||||
func NewEngine() *AlertingService {
|
||||
e := &AlertingService{}
|
||||
e.Init()
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Engine) Run(ctx context.Context) error {
|
||||
e.log.Info("Initializing Alerting")
|
||||
func (e *AlertingService) IsDisabled() bool {
|
||||
return !setting.AlertingEnabled || !setting.ExecuteAlerts
|
||||
}
|
||||
|
||||
func (e *AlertingService) Init() error {
|
||||
e.ticker = NewTicker(time.Now(), time.Second*0, clock.New())
|
||||
e.execQueue = make(chan *Job, 1000)
|
||||
e.scheduler = NewScheduler()
|
||||
e.evalHandler = NewEvalHandler()
|
||||
e.ruleReader = NewRuleReader()
|
||||
e.log = log.New("alerting.engine")
|
||||
e.resultHandler = NewResultHandler()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *AlertingService) Run(ctx context.Context) error {
|
||||
alertGroup, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
alertGroup.Go(func() error { return e.alertingTicker(ctx) })
|
||||
alertGroup.Go(func() error { return e.runJobDispatcher(ctx) })
|
||||
|
||||
err := alertGroup.Wait()
|
||||
|
||||
e.log.Info("Stopped Alerting", "reason", err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
|
||||
func (e *AlertingService) alertingTicker(grafanaCtx context.Context) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Scheduler Panic: stopping alertingTicker", "error", err, "stack", log.Stack(1))
|
||||
@@ -78,7 +86,7 @@ func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
|
||||
func (e *AlertingService) runJobDispatcher(grafanaCtx context.Context) error {
|
||||
dispatcherGroup, alertCtx := errgroup.WithContext(grafanaCtx)
|
||||
|
||||
for {
|
||||
@@ -92,13 +100,13 @@ func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
|
||||
}
|
||||
|
||||
var (
|
||||
unfinishedWorkTimeout time.Duration = time.Second * 5
|
||||
unfinishedWorkTimeout = time.Second * 5
|
||||
// TODO: Make alertTimeout and alertMaxAttempts configurable in the config file.
|
||||
alertTimeout time.Duration = time.Second * 30
|
||||
alertMaxAttempts int = 3
|
||||
alertTimeout = time.Second * 30
|
||||
alertMaxAttempts = 3
|
||||
)
|
||||
|
||||
func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
|
||||
func (e *AlertingService) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
|
||||
@@ -133,7 +141,7 @@ func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
|
||||
func (e *AlertingService) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
|
||||
job.Running = false
|
||||
close(cancelChan)
|
||||
for cancelFn := range cancelChan {
|
||||
@@ -142,7 +150,7 @@ func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Engine) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
|
||||
func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type FakeEvalHandler struct {
|
||||
SuccessCallID int // 0 means never sucess
|
||||
SuccessCallID int // 0 means never success
|
||||
CallNb int
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestEngineProcessJob(t *testing.T) {
|
||||
|
||||
Convey("Should trigger as many retries as needed", func() {
|
||||
|
||||
Convey("never sucess -> max retries number", func() {
|
||||
Convey("never success -> max retries number", func() {
|
||||
expectedAttempts := alertMaxAttempts
|
||||
evalHandler := NewFakeEvalHandler(0)
|
||||
engine.evalHandler = evalHandler
|
||||
@@ -96,7 +96,7 @@ func TestEngineProcessJob(t *testing.T) {
|
||||
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
|
||||
})
|
||||
|
||||
Convey("always sucess -> never retry", func() {
|
||||
Convey("always success -> never retry", func() {
|
||||
expectedAttempts := 1
|
||||
evalHandler := NewFakeEvalHandler(1)
|
||||
engine.evalHandler = evalHandler
|
||||
@@ -105,7 +105,7 @@ func TestEngineProcessJob(t *testing.T) {
|
||||
So(evalHandler.CallNb, ShouldEqual, expectedAttempts)
|
||||
})
|
||||
|
||||
Convey("some errors before sucess -> some retries", func() {
|
||||
Convey("some errors before success -> some retries", func() {
|
||||
expectedAttempts := int(math.Ceil(float64(alertMaxAttempts) / 2))
|
||||
evalHandler := NewFakeEvalHandler(expectedAttempts)
|
||||
engine.evalHandler = evalHandler
|
||||
|
||||
@@ -106,11 +106,11 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
|
||||
return setting.AppUrl, nil
|
||||
}
|
||||
|
||||
if ref, err := c.GetDashboardUID(); err != nil {
|
||||
ref, err := c.GetDashboardUID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
|
||||
}
|
||||
return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
|
||||
}
|
||||
|
||||
func (c *EvalContext) GetNewState() m.AlertStateType {
|
||||
|
||||
@@ -11,76 +11,78 @@ import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
// DashAlertExtractor extracts alerts from the dashboard json
|
||||
type DashAlertExtractor struct {
|
||||
Dash *m.Dashboard
|
||||
OrgId int64
|
||||
OrgID int64
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewDashAlertExtractor(dash *m.Dashboard, orgId int64) *DashAlertExtractor {
|
||||
// NewDashAlertExtractor returns a new DashAlertExtractor
|
||||
func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor {
|
||||
return &DashAlertExtractor{
|
||||
Dash: dash,
|
||||
OrgId: orgId,
|
||||
OrgID: orgID,
|
||||
log: log.New("alerting.extractor"),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) lookupDatasourceId(dsName string) (*m.DataSource, error) {
|
||||
func (e *DashAlertExtractor) lookupDatasourceID(dsName string) (*m.DataSource, error) {
|
||||
if dsName == "" {
|
||||
query := &m.GetDataSourcesQuery{OrgId: e.OrgId}
|
||||
query := &m.GetDataSourcesQuery{OrgId: e.OrgID}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for _, ds := range query.Result {
|
||||
if ds.IsDefault {
|
||||
return ds, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, ds := range query.Result {
|
||||
if ds.IsDefault {
|
||||
return ds, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgId}
|
||||
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgID}
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
return query.Result, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("Could not find datasource id for " + dsName)
|
||||
}
|
||||
|
||||
func findPanelQueryByRefId(panel *simplejson.Json, refId string) *simplejson.Json {
|
||||
func findPanelQueryByRefID(panel *simplejson.Json, refID string) *simplejson.Json {
|
||||
for _, targetsObj := range panel.Get("targets").MustArray() {
|
||||
target := simplejson.NewFromAny(targetsObj)
|
||||
|
||||
if target.Get("refId").MustString() == refId {
|
||||
if target.Get("refId").MustString() == refID {
|
||||
return target
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyJson(in *simplejson.Json) (*simplejson.Json, error) {
|
||||
rawJson, err := in.MarshalJSON()
|
||||
func copyJSON(in *simplejson.Json) (*simplejson.Json, error) {
|
||||
rawJSON, err := in.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return simplejson.NewJson(rawJson)
|
||||
return simplejson.NewJson(rawJSON)
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json) ([]*m.Alert, error) {
|
||||
func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, validateAlertFunc func(*m.Alert) bool) ([]*m.Alert, error) {
|
||||
alerts := make([]*m.Alert, 0)
|
||||
|
||||
for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
|
||||
panel := simplejson.NewFromAny(panelObj)
|
||||
|
||||
collapsedJson, collapsed := panel.CheckGet("collapsed")
|
||||
collapsedJSON, collapsed := panel.CheckGet("collapsed")
|
||||
// check if the panel is collapsed
|
||||
if collapsed && collapsedJson.MustBool() {
|
||||
if collapsed && collapsedJSON.MustBool() {
|
||||
|
||||
// extract alerts from sub panels for collapsed panels
|
||||
als, err := e.GetAlertFromPanels(panel)
|
||||
als, err := e.getAlertFromPanels(panel, validateAlertFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -95,14 +97,14 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
||||
continue
|
||||
}
|
||||
|
||||
panelId, err := panel.Get("id").Int64()
|
||||
panelID, err := panel.Get("id").Int64()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("panel id is required. err %v", err)
|
||||
}
|
||||
|
||||
// backward compatibility check, can be removed later
|
||||
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
|
||||
if hasEnabled && enabled.MustBool() == false {
|
||||
if hasEnabled && !enabled.MustBool() {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -113,8 +115,8 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
||||
|
||||
alert := &m.Alert{
|
||||
DashboardId: e.Dash.Id,
|
||||
OrgId: e.OrgId,
|
||||
PanelId: panelId,
|
||||
OrgId: e.OrgID,
|
||||
PanelId: panelID,
|
||||
Id: jsonAlert.Get("id").MustInt64(),
|
||||
Name: jsonAlert.Get("name").MustString(),
|
||||
Handler: jsonAlert.Get("handler").MustInt64(),
|
||||
@@ -126,11 +128,11 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
||||
jsonCondition := simplejson.NewFromAny(condition)
|
||||
|
||||
jsonQuery := jsonCondition.Get("query")
|
||||
queryRefId := jsonQuery.Get("params").MustArray()[0].(string)
|
||||
panelQuery := findPanelQueryByRefId(panel, queryRefId)
|
||||
queryRefID := jsonQuery.Get("params").MustArray()[0].(string)
|
||||
panelQuery := findPanelQueryByRefID(panel, queryRefID)
|
||||
|
||||
if panelQuery == nil {
|
||||
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefId)
|
||||
reason := fmt.Sprintf("Alert on PanelId: %v refers to query(%s) that cannot be found", alert.PanelId, queryRefID)
|
||||
return nil, ValidationError{Reason: reason}
|
||||
}
|
||||
|
||||
@@ -141,12 +143,13 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
||||
dsName = panel.Get("datasource").MustString()
|
||||
}
|
||||
|
||||
if datasource, err := e.lookupDatasourceId(dsName); err != nil {
|
||||
datasource, err := e.lookupDatasourceID(dsName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
|
||||
}
|
||||
|
||||
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
|
||||
|
||||
if interval, err := panel.Get("interval").String(); err == nil {
|
||||
panelQuery.Set("interval", interval)
|
||||
}
|
||||
@@ -162,21 +165,28 @@ func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if alert.ValidToSave() {
|
||||
alerts = append(alerts, alert)
|
||||
} else {
|
||||
if !validateAlertFunc(alert) {
|
||||
e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId)
|
||||
return nil, m.ErrDashboardContainsInvalidAlertData
|
||||
}
|
||||
|
||||
alerts = append(alerts, alert)
|
||||
}
|
||||
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
e.log.Debug("GetAlerts")
|
||||
func validateAlertRule(alert *m.Alert) bool {
|
||||
return alert.ValidToSave()
|
||||
}
|
||||
|
||||
dashboardJson, err := copyJson(e.Dash.Data)
|
||||
// GetAlerts extracts alerts from the dashboard json and does full validation on the alert json data
|
||||
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
return e.extractAlerts(validateAlertRule)
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractor) extractAlerts(validateFunc func(alert *m.Alert) bool) ([]*m.Alert, error) {
|
||||
dashboardJSON, err := copyJSON(e.Dash.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -185,11 +195,11 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
|
||||
// We extract alerts from rows to be backwards compatible
|
||||
// with the old dashboard json model.
|
||||
rows := dashboardJson.Get("rows").MustArray()
|
||||
rows := dashboardJSON.Get("rows").MustArray()
|
||||
if len(rows) > 0 {
|
||||
for _, rowObj := range rows {
|
||||
row := simplejson.NewFromAny(rowObj)
|
||||
a, err := e.GetAlertFromPanels(row)
|
||||
a, err := e.getAlertFromPanels(row, validateFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -197,7 +207,7 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
alerts = append(alerts, a...)
|
||||
}
|
||||
} else {
|
||||
a, err := e.GetAlertFromPanels(dashboardJson)
|
||||
a, err := e.getAlertFromPanels(dashboardJSON, validateFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -208,3 +218,10 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||
e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))
|
||||
return alerts, nil
|
||||
}
|
||||
|
||||
// ValidateAlerts validates alerts in the dashboard json but does not require a valid dashboard id
|
||||
// in the first validation pass
|
||||
func (e *DashAlertExtractor) ValidateAlerts() error {
|
||||
_, err := e.extractAlerts(func(alert *m.Alert) bool { return alert.OrgId != 0 && alert.PanelId != 0 })
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -240,5 +240,26 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
So(len(alerts), ShouldEqual, 4)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Parse and validate dashboard without id and containing an alert", func() {
|
||||
json, err := ioutil.ReadFile("./test-data/dash-without-id.json")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
dashJSON, err := simplejson.NewJson(json)
|
||||
So(err, ShouldBeNil)
|
||||
dash := m.NewDashboardFromJson(dashJSON)
|
||||
extractor := NewDashAlertExtractor(dash, 1)
|
||||
|
||||
err = extractor.ValidateAlerts()
|
||||
|
||||
Convey("Should validate without error", func() {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should fail on save", func() {
|
||||
_, err := extractor.GetAlerts()
|
||||
So(err, ShouldEqual, m.ErrDashboardContainsInvalidAlertData)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,17 +87,17 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
||||
IsAlertContext: true,
|
||||
}
|
||||
|
||||
if ref, err := context.GetDashboardUID(); err != nil {
|
||||
ref, err := context.GetDashboardUID()
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId)
|
||||
}
|
||||
renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId)
|
||||
|
||||
if imagePath, err := renderer.RenderToPng(renderOpts); err != nil {
|
||||
imagePath, err := renderer.RenderToPng(renderOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
context.ImageOnDiskPath = imagePath
|
||||
}
|
||||
context.ImageOnDiskPath = imagePath
|
||||
|
||||
context.ImagePublicUrl, err = uploader.Upload(context.Ctx, context.ImageOnDiskPath)
|
||||
if err != nil {
|
||||
@@ -117,12 +117,12 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []
|
||||
|
||||
var result []Notifier
|
||||
for _, notification := range query.Result {
|
||||
if not, err := n.createNotifierFor(notification); err != nil {
|
||||
not, err := n.createNotifierFor(notification)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if not.ShouldNotify(context) {
|
||||
result = append(result, not)
|
||||
}
|
||||
}
|
||||
if not.ShouldNotify(context) {
|
||||
result = append(result, not)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Not
|
||||
|
||||
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
|
||||
|
||||
var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)
|
||||
var notifierFactories = make(map[string]*NotifierPlugin)
|
||||
|
||||
func RegisterNotifier(plugin *NotifierPlugin) {
|
||||
notifierFactories[plugin.Type] = plugin
|
||||
|
||||
@@ -72,7 +72,10 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name)
|
||||
}
|
||||
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
body, err := bodyJSON.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: this.Url,
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "discord",
|
||||
Name: "Discord",
|
||||
Description: "Sends notifications to Discord",
|
||||
Factory: NewDiscordNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Discord settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Webhook URL</span>
|
||||
<input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.url" placeholder="Discord webhook URL"></input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
||||
func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
url := model.Settings.Get("url").MustString()
|
||||
if url == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find webhook url property in settings"}
|
||||
}
|
||||
|
||||
return &DiscordNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
WebhookURL: url,
|
||||
log: log.New("alerting.notifier.discord"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type DiscordNotifier struct {
|
||||
NotifierBase
|
||||
WebhookURL string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Sending alert notification to", "webhook_url", this.WebhookURL)
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
this.log.Error("Failed get rule link", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("username", "Grafana")
|
||||
|
||||
fields := make([]map[string]interface{}, 0)
|
||||
|
||||
for _, evt := range evalContext.EvalMatches {
|
||||
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"name": evt.Metric,
|
||||
"value": evt.Value.FullString(),
|
||||
"inline": true,
|
||||
})
|
||||
}
|
||||
|
||||
footer := map[string]interface{}{
|
||||
"text": "Grafana v" + setting.BuildVersion,
|
||||
"icon_url": "https://grafana.com/assets/img/fav32.png",
|
||||
}
|
||||
|
||||
color, _ := strconv.ParseInt(strings.TrimLeft(evalContext.GetStateModel().Color, "#"), 16, 0)
|
||||
|
||||
embed := simplejson.New()
|
||||
embed.Set("title", evalContext.GetNotificationTitle())
|
||||
//Discord takes integer for color
|
||||
embed.Set("color", color)
|
||||
embed.Set("url", ruleUrl)
|
||||
embed.Set("description", evalContext.Rule.Message)
|
||||
embed.Set("type", "rich")
|
||||
embed.Set("fields", fields)
|
||||
embed.Set("footer", footer)
|
||||
|
||||
var image map[string]interface{}
|
||||
var embeddedImage = false
|
||||
|
||||
if evalContext.ImagePublicUrl != "" {
|
||||
image = map[string]interface{}{
|
||||
"url": evalContext.ImagePublicUrl,
|
||||
}
|
||||
embed.Set("image", image)
|
||||
} else {
|
||||
image = map[string]interface{}{
|
||||
"url": "attachment://graph.png",
|
||||
}
|
||||
embed.Set("image", image)
|
||||
embeddedImage = true
|
||||
}
|
||||
|
||||
bodyJSON.Set("embeds", []interface{}{embed})
|
||||
|
||||
json, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
content_type := "application/json"
|
||||
|
||||
var body []byte
|
||||
|
||||
if embeddedImage {
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
w := multipart.NewWriter(&b)
|
||||
|
||||
f, err := os.Open(evalContext.ImageOnDiskPath)
|
||||
|
||||
if err != nil {
|
||||
this.log.Error("Can't open graph file", err)
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
fw, err := w.CreateFormField("payload_json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = fw.Write([]byte(string(json))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fw, err = w.CreateFormFile("file", "graph.png")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = io.Copy(fw, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Close()
|
||||
|
||||
body = b.Bytes()
|
||||
content_type = w.FormDataContentType()
|
||||
|
||||
} else {
|
||||
body = json
|
||||
}
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: this.WebhookURL,
|
||||
Body: string(body),
|
||||
HttpMethod: "POST",
|
||||
ContentType: content_type,
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send notification to Discord", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDiscordNotifier(t *testing.T) {
|
||||
Convey("Telegram notifier tests", t, func() {
|
||||
|
||||
Convey("Parsing alert notification from settings", func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "discord_testing",
|
||||
Type: "discord",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewDiscordNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("settings should trigger incident", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "https://web.hook/"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "discord_testing",
|
||||
Type: "discord",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewDiscordNotifier(model)
|
||||
discordNotifier := not.(*DiscordNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(discordNotifier.Name, ShouldEqual, "discord_testing")
|
||||
So(discordNotifier.Type, ShouldEqual, "discord")
|
||||
So(discordNotifier.WebhookURL, ShouldEqual, "https://web.hook/")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
}
|
||||
|
||||
message := ""
|
||||
if evalContext.Rule.State != models.AlertStateOK { //dont add message when going back to alert state ok.
|
||||
if evalContext.Rule.State != models.AlertStateOK { //don't add message when going back to alert state ok.
|
||||
message += " " + evalContext.Rule.Message
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ func (this *LineNotifier) createAlert(evalContext *alerting.EvalContext) error {
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send notification to LINE", "error", err, "body", string(body))
|
||||
this.log.Error("Failed to send notification to LINE", "error", err, "body", body)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
opsgenieAlertURL string = "https://api.opsgenie.com/v2/alerts"
|
||||
opsgenieAlertURL = "https://api.opsgenie.com/v2/alerts"
|
||||
)
|
||||
|
||||
func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
|
||||
@@ -40,7 +40,7 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
pagerdutyEventApiUrl string = "https://events.pagerduty.com/v2/enqueue"
|
||||
pagerdutyEventApiUrl = "https://events.pagerduty.com/v2/enqueue"
|
||||
)
|
||||
|
||||
func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
|
||||
@@ -129,7 +129,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
}
|
||||
|
||||
message := this.Mention
|
||||
if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
|
||||
if evalContext.Rule.State != m.AlertStateOK { //don't add message when going back to alert state ok.
|
||||
message += " " + evalContext.Rule.Message
|
||||
}
|
||||
image_url := ""
|
||||
|
||||
@@ -13,7 +13,7 @@ func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "teams",
|
||||
Name: "Microsoft Teams",
|
||||
Description: "Sends notifications using Incomming Webhook connector to Microsoft Teams",
|
||||
Description: "Sends notifications using Incoming Webhook connector to Microsoft Teams",
|
||||
Factory: NewTeamsNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">Teams settings</h3>
|
||||
|
||||
@@ -3,13 +3,14 @@ package notifiers
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
|
||||
"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/alerting"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -17,7 +18,7 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
telegramApiUrl string = "https://api.telegram.org/bot%s/%s"
|
||||
telegramApiUrl = "https://api.telegram.org/bot%s/%s"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -90,9 +91,8 @@ func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, se
|
||||
cmd, err := this.buildMessageInlineImage(evalContext)
|
||||
if err == nil {
|
||||
return cmd
|
||||
} else {
|
||||
this.log.Error("Could not generate Telegram message with inline image.", "err", err)
|
||||
}
|
||||
this.log.Error("Could not generate Telegram message with inline image.", "err", err)
|
||||
}
|
||||
|
||||
return this.buildMessageLinkedImage(evalContext)
|
||||
@@ -133,6 +133,9 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval
|
||||
}
|
||||
|
||||
ruleUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metrics := generateMetricsMessage(evalContext)
|
||||
message := generateImageCaption(evalContext, ruleUrl, metrics)
|
||||
@@ -219,7 +222,7 @@ func appendIfPossible(message string, extra string, sizeLimit int) string {
|
||||
|
||||
func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
var cmd *m.SendWebhookSync
|
||||
if evalContext.ImagePublicUrl == "" && this.UploadImage == true {
|
||||
if evalContext.ImagePublicUrl == "" && this.UploadImage {
|
||||
cmd = this.buildMessage(evalContext, true)
|
||||
} else {
|
||||
cmd = this.buildMessage(evalContext, false)
|
||||
|
||||
@@ -100,7 +100,7 @@ func TestTelegramNotifier(t *testing.T) {
|
||||
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() {
|
||||
Convey("Metrics should be skipped if they don't 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 ",
|
||||
|
||||
@@ -83,27 +83,6 @@ func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
fields := make([]map[string]interface{}, 0)
|
||||
fieldLimitCount := 4
|
||||
for index, evt := range evalContext.EvalMatches {
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"title": evt.Metric,
|
||||
"value": evt.Value,
|
||||
"short": true,
|
||||
})
|
||||
if index > fieldLimitCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if evalContext.Error != nil {
|
||||
fields = append(fields, map[string]interface{}{
|
||||
"title": "Error message",
|
||||
"value": evalContext.Error.Error(),
|
||||
"short": false,
|
||||
})
|
||||
}
|
||||
|
||||
messageType := evalContext.Rule.State
|
||||
if evalContext.Rule.State == models.AlertStateAlerting { // translate 'Alerting' to 'CRITICAL' (Victorops analog)
|
||||
messageType = AlertStateCritical
|
||||
|
||||
@@ -16,7 +16,7 @@ type RuleReader interface {
|
||||
|
||||
type DefaultRuleReader struct {
|
||||
sync.RWMutex
|
||||
serverID string
|
||||
//serverID string
|
||||
serverPosition int
|
||||
clusterSize int
|
||||
log log.Logger
|
||||
|
||||
@@ -56,7 +56,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
|
||||
if err := bus.Dispatch(cmd); err != nil {
|
||||
if err == m.ErrCannotChangeStateOnPausedAlert {
|
||||
handler.log.Error("Cannot change state on alert thats pause", "error", err)
|
||||
handler.log.Error("Cannot change state on alert that's paused", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
|
||||
Text: "",
|
||||
NewState: string(evalContext.Rule.State),
|
||||
PrevState: string(evalContext.PrevAlertState),
|
||||
Epoch: time.Now().Unix(),
|
||||
Epoch: time.Now().UnixNano() / int64(time.Millisecond),
|
||||
Data: annotationData,
|
||||
}
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ func (e ValidationError) Error() string {
|
||||
}
|
||||
|
||||
var (
|
||||
ValueFormatRegex = regexp.MustCompile("^\\d+")
|
||||
UnitFormatRegex = regexp.MustCompile("\\w{1}$")
|
||||
ValueFormatRegex = regexp.MustCompile(`^\d+`)
|
||||
UnitFormatRegex = regexp.MustCompile(`\w{1}$`)
|
||||
)
|
||||
|
||||
var unitMultiplier = map[string]int{
|
||||
@@ -103,25 +103,25 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
|
||||
|
||||
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
|
||||
jsonModel := simplejson.NewFromAny(v)
|
||||
if id, err := jsonModel.Get("id").Int64(); err != nil {
|
||||
id, err := jsonModel.Get("id").Int64()
|
||||
if err != nil {
|
||||
return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
|
||||
} else {
|
||||
model.Notifications = append(model.Notifications, id)
|
||||
}
|
||||
model.Notifications = append(model.Notifications, id)
|
||||
}
|
||||
|
||||
for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {
|
||||
conditionModel := simplejson.NewFromAny(condition)
|
||||
conditionType := conditionModel.Get("type").MustString()
|
||||
if factory, exist := conditionFactories[conditionType]; !exist {
|
||||
factory, exist := conditionFactories[conditionType]
|
||||
if !exist {
|
||||
return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
|
||||
} else {
|
||||
if queryCondition, err := factory(conditionModel, index); err != nil {
|
||||
return nil, ValidationError{Err: err, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
|
||||
} else {
|
||||
model.Conditions = append(model.Conditions, queryCondition)
|
||||
}
|
||||
}
|
||||
queryCondition, err := factory(conditionModel, index)
|
||||
if err != nil {
|
||||
return nil, ValidationError{Err: err, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
|
||||
}
|
||||
model.Conditions = append(model.Conditions, queryCondition)
|
||||
}
|
||||
|
||||
if len(model.Conditions) == 0 {
|
||||
@@ -133,7 +133,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
|
||||
|
||||
type ConditionFactory func(model *simplejson.Json, index int) (Condition, error)
|
||||
|
||||
var conditionFactories map[string]ConditionFactory = make(map[string]ConditionFactory)
|
||||
var conditionFactories = make(map[string]ConditionFactory)
|
||||
|
||||
func RegisterCondition(typeName string, factory ConditionFactory) {
|
||||
conditionFactories[typeName] = factory
|
||||
|
||||
@@ -15,7 +15,7 @@ type SchedulerImpl struct {
|
||||
|
||||
func NewScheduler() Scheduler {
|
||||
return &SchedulerImpl{
|
||||
jobs: make(map[int64]*Job, 0),
|
||||
jobs: make(map[int64]*Job),
|
||||
log: log.New("alerting.scheduler"),
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ func NewScheduler() Scheduler {
|
||||
func (s *SchedulerImpl) Update(rules []*Rule) {
|
||||
s.log.Debug("Scheduling update", "ruleCount", len(rules))
|
||||
|
||||
jobs := make(map[int64]*Job, 0)
|
||||
jobs := make(map[int64]*Job)
|
||||
|
||||
for i, rule := range rules {
|
||||
var job *Job
|
||||
@@ -58,7 +58,7 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) {
|
||||
|
||||
if job.OffsetWait && now%job.Offset == 0 {
|
||||
job.OffsetWait = false
|
||||
s.enque(job, execQueue)
|
||||
s.enqueue(job, execQueue)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -66,13 +66,13 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *Job) {
|
||||
if job.Offset > 0 {
|
||||
job.OffsetWait = true
|
||||
} else {
|
||||
s.enque(job, execQueue)
|
||||
s.enqueue(job, execQueue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SchedulerImpl) enque(job *Job, execQueue chan *Job) {
|
||||
func (s *SchedulerImpl) enqueue(job *Job, execQueue chan *Job) {
|
||||
s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name, "id", job.Rule.Id)
|
||||
execQueue <- job
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -279,4 +279,4 @@
|
||||
"version": 120,
|
||||
"links": [],
|
||||
"gnetId": null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ type ItemQuery struct {
|
||||
OrgId int64 `json:"orgId"`
|
||||
From int64 `json:"from"`
|
||||
To int64 `json:"to"`
|
||||
UserId int64 `json:"userId"`
|
||||
AlertId int64 `json:"alertId"`
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
PanelId int64 `json:"panelId"`
|
||||
@@ -63,6 +64,8 @@ type Item struct {
|
||||
PrevState string `json:"prevState"`
|
||||
NewState string `json:"newState"`
|
||||
Epoch int64 `json:"epoch"`
|
||||
Created int64 `json:"created"`
|
||||
Updated int64 `json:"updated"`
|
||||
Tags []string `json:"tags"`
|
||||
Data *simplejson.Json `json:"data"`
|
||||
|
||||
@@ -80,6 +83,8 @@ type ItemDTO struct {
|
||||
UserId int64 `json:"userId"`
|
||||
NewState string `json:"newState"`
|
||||
PrevState string `json:"prevState"`
|
||||
Created int64 `json:"created"`
|
||||
Updated int64 `json:"updated"`
|
||||
Time int64 `json:"time"`
|
||||
Text string `json:"text"`
|
||||
RegionId int64 `json:"regionId"`
|
||||
|
||||
@@ -7,60 +7,52 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type CleanUpService struct {
|
||||
log log.Logger
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
}
|
||||
|
||||
func NewCleanUpService() *CleanUpService {
|
||||
return &CleanUpService{
|
||||
log: log.New("cleanup"),
|
||||
}
|
||||
func init() {
|
||||
registry.RegisterService(&CleanUpService{})
|
||||
}
|
||||
|
||||
func (service *CleanUpService) Run(ctx context.Context) error {
|
||||
service.log.Info("Initializing CleanUpService")
|
||||
|
||||
g, _ := errgroup.WithContext(ctx)
|
||||
g.Go(func() error { return service.start(ctx) })
|
||||
|
||||
err := g.Wait()
|
||||
service.log.Info("Stopped CleanUpService", "reason", err)
|
||||
return err
|
||||
func (srv *CleanUpService) Init() error {
|
||||
srv.log = log.New("cleanup")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *CleanUpService) start(ctx context.Context) error {
|
||||
service.cleanUpTmpFiles()
|
||||
func (srv *CleanUpService) Run(ctx context.Context) error {
|
||||
srv.cleanUpTmpFiles()
|
||||
|
||||
ticker := time.NewTicker(time.Minute * 10)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
service.cleanUpTmpFiles()
|
||||
service.deleteExpiredSnapshots()
|
||||
service.deleteExpiredDashboardVersions()
|
||||
service.deleteOldLoginAttempts()
|
||||
srv.cleanUpTmpFiles()
|
||||
srv.deleteExpiredSnapshots()
|
||||
srv.deleteExpiredDashboardVersions()
|
||||
srv.deleteOldLoginAttempts()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (service *CleanUpService) cleanUpTmpFiles() {
|
||||
if _, err := os.Stat(setting.ImagesDir); os.IsNotExist(err) {
|
||||
func (srv *CleanUpService) cleanUpTmpFiles() {
|
||||
if _, err := os.Stat(srv.Cfg.ImagesDir); os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(setting.ImagesDir)
|
||||
files, err := ioutil.ReadDir(srv.Cfg.ImagesDir)
|
||||
if err != nil {
|
||||
service.log.Error("Problem reading image dir", "error", err)
|
||||
srv.log.Error("Problem reading image dir", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -72,36 +64,36 @@ func (service *CleanUpService) cleanUpTmpFiles() {
|
||||
}
|
||||
|
||||
for _, file := range toDelete {
|
||||
fullPath := path.Join(setting.ImagesDir, file.Name())
|
||||
fullPath := path.Join(srv.Cfg.ImagesDir, file.Name())
|
||||
err := os.Remove(fullPath)
|
||||
if err != nil {
|
||||
service.log.Error("Failed to delete temp file", "file", file.Name(), "error", err)
|
||||
srv.log.Error("Failed to delete temp file", "file", file.Name(), "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
service.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "keept", len(files))
|
||||
srv.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "keept", len(files))
|
||||
}
|
||||
|
||||
func (service *CleanUpService) deleteExpiredSnapshots() {
|
||||
func (srv *CleanUpService) deleteExpiredSnapshots() {
|
||||
cmd := m.DeleteExpiredSnapshotsCommand{}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
service.log.Error("Failed to delete expired snapshots", "error", err.Error())
|
||||
srv.log.Error("Failed to delete expired snapshots", "error", err.Error())
|
||||
} else {
|
||||
service.log.Debug("Deleted expired snapshots", "rows affected", cmd.DeletedRows)
|
||||
srv.log.Debug("Deleted expired snapshots", "rows affected", cmd.DeletedRows)
|
||||
}
|
||||
}
|
||||
|
||||
func (service *CleanUpService) deleteExpiredDashboardVersions() {
|
||||
func (srv *CleanUpService) deleteExpiredDashboardVersions() {
|
||||
cmd := m.DeleteExpiredVersionsCommand{}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
service.log.Error("Failed to delete expired dashboard versions", "error", err.Error())
|
||||
srv.log.Error("Failed to delete expired dashboard versions", "error", err.Error())
|
||||
} else {
|
||||
service.log.Debug("Deleted old/expired dashboard versions", "rows affected", cmd.DeletedRows)
|
||||
srv.log.Debug("Deleted old/expired dashboard versions", "rows affected", cmd.DeletedRows)
|
||||
}
|
||||
}
|
||||
|
||||
func (service *CleanUpService) deleteOldLoginAttempts() {
|
||||
if setting.DisableBruteForceLoginProtection {
|
||||
func (srv *CleanUpService) deleteOldLoginAttempts() {
|
||||
if srv.Cfg.DisableBruteForceLoginProtection {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -109,8 +101,8 @@ func (service *CleanUpService) deleteOldLoginAttempts() {
|
||||
OlderThan: time.Now().Add(time.Minute * -10),
|
||||
}
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
service.log.Error("Problem deleting expired login attempts", "error", err.Error())
|
||||
srv.log.Error("Problem deleting expired login attempts", "error", err.Error())
|
||||
} else {
|
||||
service.log.Debug("Deleted expired login attempts", "rows affected", cmd.DeletedRows)
|
||||
srv.log.Debug("Deleted expired login attempts", "rows affected", cmd.DeletedRows)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*mod
|
||||
return cmd.Result, nil
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool) (*models.SaveDashboardCommand, error) {
|
||||
func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) {
|
||||
dash := dto.Dashboard
|
||||
|
||||
dash.Title = strings.TrimSpace(dash.Title)
|
||||
@@ -103,6 +103,29 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if validateBeforeSaveCmd.Result.IsParentFolderChanged {
|
||||
folderGuardian := guardian.New(dash.FolderId, dto.OrgId, dto.User)
|
||||
if canSave, err := folderGuardian.CanSave(); err != nil || !canSave {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, models.ErrDashboardUpdateAccessDenied
|
||||
}
|
||||
}
|
||||
|
||||
if validateProvisionedDashboard {
|
||||
isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dash.Id}
|
||||
err := bus.Dispatch(isDashboardProvisioned)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isDashboardProvisioned.Result {
|
||||
return nil, models.ErrDashboardCannotSaveProvisionedDashboard
|
||||
}
|
||||
}
|
||||
|
||||
guard := guardian.New(dash.GetDashboardIdForSavePermissionCheck(), dto.OrgId, dto.User)
|
||||
if canSave, err := guard.CanSave(); err != nil || !canSave {
|
||||
if err != nil {
|
||||
@@ -148,7 +171,7 @@ func (dr *dashboardServiceImpl) SaveProvisionedDashboard(dto *SaveDashboardDTO,
|
||||
UserId: 0,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
}
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, true)
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, true, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -178,7 +201,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
|
||||
UserId: 0,
|
||||
OrgRole: models.ROLE_ADMIN,
|
||||
}
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, false)
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -197,7 +220,7 @@ func (dr *dashboardServiceImpl) SaveFolderForProvisionedDashboards(dto *SaveDash
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, true)
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, true, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -216,7 +239,7 @@ func (dr *dashboardServiceImpl) SaveDashboard(dto *SaveDashboardDTO) (*models.Da
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) ImportDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) {
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, false)
|
||||
cmd, err := dr.buildSaveDashboardCommand(dto, false, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ import (
|
||||
|
||||
func TestDashboardService(t *testing.T) {
|
||||
Convey("Dashboard service tests", t, func() {
|
||||
service := dashboardServiceImpl{}
|
||||
bus.ClearBusHandlers()
|
||||
|
||||
service := &dashboardServiceImpl{}
|
||||
|
||||
origNewDashboardGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
||||
@@ -51,6 +53,12 @@ func TestDashboardService(t *testing.T) {
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
|
||||
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
cmd.Result = false
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -72,12 +80,42 @@ func TestDashboardService(t *testing.T) {
|
||||
dto.Dashboard.SetUid(tc.Uid)
|
||||
dto.User = &models.SignedInUser{}
|
||||
|
||||
_, err := service.buildSaveDashboardCommand(dto, true)
|
||||
_, err := service.buildSaveDashboardCommand(dto, true, false)
|
||||
So(err, ShouldEqual, tc.Error)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Should return validation error if dashboard is provisioned", func() {
|
||||
provisioningValidated := false
|
||||
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
provisioningValidated = true
|
||||
cmd.Result = true
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
|
||||
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
|
||||
return nil
|
||||
})
|
||||
|
||||
dto.Dashboard = models.NewDashboard("Dash")
|
||||
dto.Dashboard.SetId(3)
|
||||
dto.User = &models.SignedInUser{UserId: 1}
|
||||
_, err := service.SaveDashboard(dto)
|
||||
So(provisioningValidated, ShouldBeTrue)
|
||||
So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard)
|
||||
})
|
||||
|
||||
Convey("Should return validation error if alert data is invalid", func() {
|
||||
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
cmd.Result = false
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
|
||||
return errors.New("error")
|
||||
})
|
||||
@@ -88,6 +126,80 @@ func TestDashboardService(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Save provisioned dashboard validation", func() {
|
||||
dto := &SaveDashboardDTO{}
|
||||
|
||||
Convey("Should not return validation error if dashboard is provisioned", func() {
|
||||
provisioningValidated := false
|
||||
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
provisioningValidated = true
|
||||
cmd.Result = true
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
|
||||
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
dto.Dashboard = models.NewDashboard("Dash")
|
||||
dto.Dashboard.SetId(3)
|
||||
dto.User = &models.SignedInUser{UserId: 1}
|
||||
_, err := service.SaveProvisionedDashboard(dto, nil)
|
||||
So(err, ShouldBeNil)
|
||||
So(provisioningValidated, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Import dashboard validation", func() {
|
||||
dto := &SaveDashboardDTO{}
|
||||
|
||||
Convey("Should return validation error if dashboard is provisioned", func() {
|
||||
provisioningValidated := false
|
||||
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
provisioningValidated = true
|
||||
cmd.Result = true
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
|
||||
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.SaveProvisionedDashboardCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.UpdateDashboardAlertsCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
dto.Dashboard = models.NewDashboard("Dash")
|
||||
dto.Dashboard.SetId(3)
|
||||
dto.User = &models.SignedInUser{UserId: 1}
|
||||
_, err := service.ImportDashboard(dto)
|
||||
So(provisioningValidated, ShouldBeTrue)
|
||||
So(err, ShouldEqual, models.ErrDashboardCannotSaveProvisionedDashboard)
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
guardian.New = origNewDashboardGuardian
|
||||
})
|
||||
|
||||
@@ -104,7 +104,7 @@ func (dr *dashboardServiceImpl) CreateFolder(cmd *models.CreateFolderCommand) er
|
||||
User: dr.user,
|
||||
}
|
||||
|
||||
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
|
||||
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false, false)
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func (dr *dashboardServiceImpl) UpdateFolder(existingUid string, cmd *models.Upd
|
||||
Overwrite: cmd.Overwrite,
|
||||
}
|
||||
|
||||
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false)
|
||||
saveDashboardCmd, err := dr.buildSaveDashboardCommand(dto, false, false)
|
||||
if err != nil {
|
||||
return toFolderError(err)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ func TestFolderService(t *testing.T) {
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
|
||||
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
|
||||
return models.ErrDashboardUpdateAccessDenied
|
||||
})
|
||||
|
||||
@@ -92,6 +93,7 @@ func TestFolderService(t *testing.T) {
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardBeforeSaveCommand) error {
|
||||
cmd.Result = &models.ValidateDashboardBeforeSaveResult{}
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -108,11 +110,19 @@ func TestFolderService(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
provisioningValidated := false
|
||||
|
||||
bus.AddHandler("test", func(query *models.IsDashboardProvisionedQuery) error {
|
||||
provisioningValidated = true
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("When creating folder should not return access denied error", func() {
|
||||
err := service.CreateFolder(&models.CreateFolderCommand{
|
||||
Title: "Folder",
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
So(provisioningValidated, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("When updating folder should not return access denied error", func() {
|
||||
@@ -121,6 +131,7 @@ func TestFolderService(t *testing.T) {
|
||||
Title: "Folder",
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
So(provisioningValidated, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("When deleting folder by uid should not return access denied error", func() {
|
||||
|
||||
@@ -113,7 +113,7 @@ func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.D
|
||||
return false, err
|
||||
}
|
||||
|
||||
// evalute team rules
|
||||
// evaluate team rules
|
||||
for _, p := range acl {
|
||||
for _, ug := range teams {
|
||||
if ug.Id == p.TeamId && p.Permission >= permission {
|
||||
@@ -154,12 +154,7 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss
|
||||
// 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 {
|
||||
if !existingPerm.Inherited {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -173,7 +168,7 @@ func (g *dashboardGuardianImpl) CheckPermissionBeforeUpdate(permission m.Permiss
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return g.checkAcl(permission, acl)
|
||||
return g.checkAcl(permission, existingPermissions)
|
||||
}
|
||||
|
||||
// GetAcl returns dashboard acl
|
||||
@@ -187,13 +182,6 @@ func (g *dashboardGuardianImpl) 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
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,256 @@
|
||||
package guardian
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
type scenarioContext struct {
|
||||
t *testing.T
|
||||
orgRoleScenario string
|
||||
permissionScenario string
|
||||
g DashboardGuardian
|
||||
givenUser *m.SignedInUser
|
||||
givenDashboardID int64
|
||||
givenPermissions []*m.DashboardAclInfoDTO
|
||||
givenTeams []*m.Team
|
||||
updatePermissions []*m.DashboardAcl
|
||||
expectedFlags permissionFlags
|
||||
callerFile string
|
||||
callerLine int
|
||||
}
|
||||
|
||||
type scenarioFunc func(c *scenarioContext)
|
||||
|
||||
func orgRoleScenario(desc string, t *testing.T, role m.RoleType, fn scenarioFunc) {
|
||||
user := &m.SignedInUser{
|
||||
UserId: userID,
|
||||
OrgId: orgID,
|
||||
OrgRole: role,
|
||||
}
|
||||
guard := New(dashboardID, orgID, user)
|
||||
sc := &scenarioContext{
|
||||
t: t,
|
||||
orgRoleScenario: desc,
|
||||
givenUser: user,
|
||||
givenDashboardID: dashboardID,
|
||||
g: guard,
|
||||
}
|
||||
|
||||
Convey(desc, func() {
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, permissions []*m.DashboardAclInfoDTO, fn scenarioFunc) {
|
||||
bus.ClearBusHandlers()
|
||||
|
||||
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||
if query.OrgId != sc.givenUser.OrgId {
|
||||
sc.reportFailure("Invalid organization id for GetDashboardAclInfoListQuery", sc.givenUser.OrgId, query.OrgId)
|
||||
}
|
||||
if query.DashboardId != sc.givenDashboardID {
|
||||
sc.reportFailure("Invalid dashboard id for GetDashboardAclInfoListQuery", sc.givenDashboardID, query.DashboardId)
|
||||
}
|
||||
|
||||
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 {
|
||||
if query.OrgId != sc.givenUser.OrgId {
|
||||
sc.reportFailure("Invalid organization id for GetTeamsByUserQuery", sc.givenUser.OrgId, query.OrgId)
|
||||
}
|
||||
if query.UserId != sc.givenUser.UserId {
|
||||
sc.reportFailure("Invalid user id for GetTeamsByUserQuery", sc.givenUser.UserId, query.UserId)
|
||||
}
|
||||
|
||||
query.Result = teams
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.permissionScenario = desc
|
||||
sc.g = New(dashboardID, sc.givenUser.OrgId, sc.givenUser)
|
||||
sc.givenDashboardID = dashboardID
|
||||
sc.givenPermissions = permissions
|
||||
sc.givenTeams = teams
|
||||
|
||||
Convey(desc, func() {
|
||||
fn(sc)
|
||||
})
|
||||
}
|
||||
|
||||
type permissionType uint8
|
||||
|
||||
const (
|
||||
USER permissionType = 1 << iota
|
||||
TEAM
|
||||
EDITOR
|
||||
VIEWER
|
||||
)
|
||||
|
||||
func (p permissionType) String() string {
|
||||
names := map[uint8]string{
|
||||
uint8(USER): "user",
|
||||
uint8(TEAM): "team",
|
||||
uint8(EDITOR): "editor role",
|
||||
uint8(VIEWER): "viewer role",
|
||||
}
|
||||
return names[uint8(p)]
|
||||
}
|
||||
|
||||
type permissionFlags uint8
|
||||
|
||||
const (
|
||||
NO_ACCESS permissionFlags = 1 << iota
|
||||
CAN_ADMIN
|
||||
CAN_EDIT
|
||||
CAN_SAVE
|
||||
CAN_VIEW
|
||||
FULL_ACCESS = CAN_ADMIN | CAN_EDIT | CAN_SAVE | CAN_VIEW
|
||||
EDITOR_ACCESS = CAN_EDIT | CAN_SAVE | CAN_VIEW
|
||||
VIEWER_ACCESS = CAN_VIEW
|
||||
)
|
||||
|
||||
func (flag permissionFlags) canAdmin() bool {
|
||||
return flag&CAN_ADMIN != 0
|
||||
}
|
||||
|
||||
func (flag permissionFlags) canEdit() bool {
|
||||
return flag&CAN_EDIT != 0
|
||||
}
|
||||
|
||||
func (flag permissionFlags) canSave() bool {
|
||||
return flag&CAN_SAVE != 0
|
||||
}
|
||||
|
||||
func (flag permissionFlags) canView() bool {
|
||||
return flag&CAN_VIEW != 0
|
||||
}
|
||||
|
||||
func (flag permissionFlags) noAccess() bool {
|
||||
return flag&(CAN_ADMIN|CAN_EDIT|CAN_SAVE|CAN_VIEW) == 0
|
||||
}
|
||||
|
||||
func (f permissionFlags) String() string {
|
||||
r := []string{}
|
||||
|
||||
if f.canAdmin() {
|
||||
r = append(r, "admin")
|
||||
}
|
||||
|
||||
if f.canEdit() {
|
||||
r = append(r, "edit")
|
||||
}
|
||||
|
||||
if f.canSave() {
|
||||
r = append(r, "save")
|
||||
}
|
||||
|
||||
if f.canView() {
|
||||
r = append(r, "view")
|
||||
}
|
||||
|
||||
if f.noAccess() {
|
||||
r = append(r, "<no access>")
|
||||
}
|
||||
|
||||
return strings.Join(r[:], ", ")
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) reportSuccess() {
|
||||
So(true, ShouldBeTrue)
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) reportFailure(desc string, expected interface{}, actual interface{}) {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("\n")
|
||||
buf.WriteString(sc.orgRoleScenario)
|
||||
buf.WriteString(" ")
|
||||
buf.WriteString(sc.permissionScenario)
|
||||
buf.WriteString("\n ")
|
||||
buf.WriteString(desc)
|
||||
buf.WriteString("\n")
|
||||
buf.WriteString(fmt.Sprintf("Source test: %s:%d\n", sc.callerFile, sc.callerLine))
|
||||
buf.WriteString(fmt.Sprintf("Expected: %v\n", expected))
|
||||
buf.WriteString(fmt.Sprintf("Actual: %v\n", actual))
|
||||
buf.WriteString("Context:")
|
||||
buf.WriteString(fmt.Sprintf("\n Given user: orgRole=%s, id=%d, orgId=%d", sc.givenUser.OrgRole, sc.givenUser.UserId, sc.givenUser.OrgId))
|
||||
buf.WriteString(fmt.Sprintf("\n Given dashboard id: %d", sc.givenDashboardID))
|
||||
|
||||
for i, p := range sc.givenPermissions {
|
||||
r := "<nil>"
|
||||
if p.Role != nil {
|
||||
r = string(*p.Role)
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("\n Given permission (%d): dashboardId=%d, userId=%d, teamId=%d, role=%v, permission=%s", i, p.DashboardId, p.UserId, p.TeamId, r, p.Permission.String()))
|
||||
}
|
||||
|
||||
for i, t := range sc.givenTeams {
|
||||
buf.WriteString(fmt.Sprintf("\n Given team (%d): id=%d", i, t.Id))
|
||||
}
|
||||
|
||||
for i, p := range sc.updatePermissions {
|
||||
r := "<nil>"
|
||||
if p.Role != nil {
|
||||
r = string(*p.Role)
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("\n Update permission (%d): dashboardId=%d, userId=%d, teamId=%d, role=%v, permission=%s", i, p.DashboardId, p.UserId, p.TeamId, r, p.Permission.String()))
|
||||
}
|
||||
|
||||
sc.t.Fatalf(buf.String())
|
||||
}
|
||||
|
||||
func newCustomUserPermission(dashboardID int64, userID int64, permission m.PermissionType) *m.DashboardAcl {
|
||||
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, UserId: userID, Permission: permission}
|
||||
}
|
||||
|
||||
func newDefaultUserPermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
|
||||
return newCustomUserPermission(dashboardID, userID, permission)
|
||||
}
|
||||
|
||||
func newCustomTeamPermission(dashboardID int64, teamID int64, permission m.PermissionType) *m.DashboardAcl {
|
||||
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, TeamId: teamID, Permission: permission}
|
||||
}
|
||||
|
||||
func newDefaultTeamPermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
|
||||
return newCustomTeamPermission(dashboardID, teamID, permission)
|
||||
}
|
||||
|
||||
func newAdminRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
|
||||
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &adminRole, Permission: permission}
|
||||
}
|
||||
|
||||
func newEditorRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
|
||||
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &editorRole, Permission: permission}
|
||||
}
|
||||
|
||||
func newViewerRolePermission(dashboardID int64, permission m.PermissionType) *m.DashboardAcl {
|
||||
return &m.DashboardAcl{OrgId: orgID, DashboardId: dashboardID, Role: &viewerRole, Permission: permission}
|
||||
}
|
||||
|
||||
func toDto(acl *m.DashboardAcl) *m.DashboardAclInfoDTO {
|
||||
return &m.DashboardAclInfoDTO{
|
||||
OrgId: acl.OrgId,
|
||||
DashboardId: acl.DashboardId,
|
||||
UserId: acl.UserId,
|
||||
TeamId: acl.TeamId,
|
||||
Role: acl.Role,
|
||||
Permission: acl.Permission,
|
||||
PermissionName: acl.Permission.String(),
|
||||
}
|
||||
}
|
||||
@@ -7,51 +7,18 @@ package notifications
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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
|
||||
|
||||
func initMailQueue() {
|
||||
mailQueue = make(chan *Message, 10)
|
||||
go processMailQueue()
|
||||
}
|
||||
|
||||
func processMailQueue() {
|
||||
for {
|
||||
select {
|
||||
case msg := <-mailQueue:
|
||||
num, err := send(msg)
|
||||
tos := strings.Join(msg.To, "; ")
|
||||
info := ""
|
||||
if err != nil {
|
||||
if len(msg.Info) > 0 {
|
||||
info = ", info: " + msg.Info
|
||||
}
|
||||
log.Error(4, fmt.Sprintf("Async sent email %d succeed, not send emails: %s%s err: %s", num, tos, info, err))
|
||||
} else {
|
||||
log.Trace(fmt.Sprintf("Async sent email %d succeed, sent emails: %s%s", num, tos, info))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var addToMailQueue = func(msg *Message) {
|
||||
mailQueue <- msg
|
||||
}
|
||||
|
||||
func send(msg *Message) (int, error) {
|
||||
dialer, err := createDialer()
|
||||
func (ns *NotificationService) send(msg *Message) (int, error) {
|
||||
dialer, err := ns.createDialer()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -75,8 +42,8 @@ func send(msg *Message) (int, error) {
|
||||
return len(msg.To), nil
|
||||
}
|
||||
|
||||
func createDialer() (*gomail.Dialer, error) {
|
||||
host, port, err := net.SplitHostPort(setting.Smtp.Host)
|
||||
func (ns *NotificationService) createDialer() (*gomail.Dialer, error) {
|
||||
host, port, err := net.SplitHostPort(ns.Cfg.Smtp.Host)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -87,30 +54,31 @@ func createDialer() (*gomail.Dialer, error) {
|
||||
}
|
||||
|
||||
tlsconfig := &tls.Config{
|
||||
InsecureSkipVerify: setting.Smtp.SkipVerify,
|
||||
InsecureSkipVerify: ns.Cfg.Smtp.SkipVerify,
|
||||
ServerName: host,
|
||||
}
|
||||
|
||||
if setting.Smtp.CertFile != "" {
|
||||
cert, err := tls.LoadX509KeyPair(setting.Smtp.CertFile, setting.Smtp.KeyFile)
|
||||
if ns.Cfg.Smtp.CertFile != "" {
|
||||
cert, err := tls.LoadX509KeyPair(ns.Cfg.Smtp.CertFile, ns.Cfg.Smtp.KeyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not load cert or key file. error: %v", err)
|
||||
}
|
||||
tlsconfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
d := gomail.NewDialer(host, iPort, setting.Smtp.User, setting.Smtp.Password)
|
||||
d := gomail.NewDialer(host, iPort, ns.Cfg.Smtp.User, ns.Cfg.Smtp.Password)
|
||||
d.TLSConfig = tlsconfig
|
||||
if setting.Smtp.EhloIdentity != "" {
|
||||
d.LocalName = setting.Smtp.EhloIdentity
|
||||
|
||||
if ns.Cfg.Smtp.EhloIdentity != "" {
|
||||
d.LocalName = ns.Cfg.Smtp.EhloIdentity
|
||||
} else {
|
||||
d.LocalName = setting.InstanceName
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
|
||||
if !setting.Smtp.Enabled {
|
||||
func (ns *NotificationService) buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
|
||||
if !ns.Cfg.Smtp.Enabled {
|
||||
return nil, m.ErrSmtpNotEnabled
|
||||
}
|
||||
|
||||
@@ -135,7 +103,7 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
|
||||
subjectText, hasSubject := subjectData["value"]
|
||||
|
||||
if !hasSubject {
|
||||
return nil, errors.New(fmt.Sprintf("Missing subject in Template %s", cmd.Template))
|
||||
return nil, fmt.Errorf("Missing subject in Template %s", cmd.Template)
|
||||
}
|
||||
|
||||
subjectTmpl, err := template.New("subject").Parse(subjectText.(string))
|
||||
@@ -154,7 +122,7 @@ func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) {
|
||||
|
||||
return &Message{
|
||||
To: cmd.To,
|
||||
From: fmt.Sprintf("%s <%s>", setting.Smtp.FromName, setting.Smtp.FromAddress),
|
||||
From: fmt.Sprintf("%s <%s>", ns.Cfg.Smtp.FromName, ns.Cfg.Smtp.FromAddress),
|
||||
Subject: subject,
|
||||
Body: buffer.String(),
|
||||
EmbededFiles: cmd.EmbededFiles,
|
||||
|
||||
@@ -7,11 +7,13 @@ import (
|
||||
"html/template"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/events"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@@ -21,33 +23,46 @@ var tmplResetPassword = "reset_password.html"
|
||||
var tmplSignUpStarted = "signup_started.html"
|
||||
var tmplWelcomeOnSignUp = "welcome_on_signup.html"
|
||||
|
||||
func Init() error {
|
||||
initMailQueue()
|
||||
initWebhookQueue()
|
||||
func init() {
|
||||
registry.RegisterService(&NotificationService{})
|
||||
}
|
||||
|
||||
bus.AddHandler("email", sendResetPasswordEmail)
|
||||
bus.AddHandler("email", validateResetPasswordCode)
|
||||
bus.AddHandler("email", sendEmailCommandHandler)
|
||||
type NotificationService struct {
|
||||
Bus bus.Bus `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
|
||||
bus.AddCtxHandler("email", sendEmailCommandHandlerSync)
|
||||
mailQueue chan *Message
|
||||
webhookQueue chan *Webhook
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
bus.AddCtxHandler("webhook", SendWebhookSync)
|
||||
func (ns *NotificationService) Init() error {
|
||||
ns.log = log.New("notifications")
|
||||
ns.mailQueue = make(chan *Message, 10)
|
||||
ns.webhookQueue = make(chan *Webhook, 10)
|
||||
|
||||
bus.AddEventListener(signUpStartedHandler)
|
||||
bus.AddEventListener(signUpCompletedHandler)
|
||||
ns.Bus.AddHandler(ns.sendResetPasswordEmail)
|
||||
ns.Bus.AddHandler(ns.validateResetPasswordCode)
|
||||
ns.Bus.AddHandler(ns.sendEmailCommandHandler)
|
||||
|
||||
ns.Bus.AddCtxHandler(ns.sendEmailCommandHandlerSync)
|
||||
ns.Bus.AddCtxHandler(ns.SendWebhookSync)
|
||||
|
||||
ns.Bus.AddEventListener(ns.signUpStartedHandler)
|
||||
ns.Bus.AddEventListener(ns.signUpCompletedHandler)
|
||||
|
||||
mailTemplates = template.New("name")
|
||||
mailTemplates.Funcs(template.FuncMap{
|
||||
"Subject": subjectTemplateFunc,
|
||||
})
|
||||
|
||||
templatePattern := filepath.Join(setting.StaticRootPath, setting.Smtp.TemplatesPattern)
|
||||
templatePattern := filepath.Join(setting.StaticRootPath, ns.Cfg.Smtp.TemplatesPattern)
|
||||
_, err := mailTemplates.ParseGlob(templatePattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !util.IsEmail(setting.Smtp.FromAddress) {
|
||||
if !util.IsEmail(ns.Cfg.Smtp.FromAddress) {
|
||||
return errors.New("Invalid email address for SMTP from_address config")
|
||||
}
|
||||
|
||||
@@ -58,14 +73,44 @@ func Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
|
||||
return sendWebRequestSync(ctx, &Webhook{
|
||||
Url: cmd.Url,
|
||||
User: cmd.User,
|
||||
Password: cmd.Password,
|
||||
Body: cmd.Body,
|
||||
HttpMethod: cmd.HttpMethod,
|
||||
HttpHeader: cmd.HttpHeader,
|
||||
func (ns *NotificationService) Run(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case webhook := <-ns.webhookQueue:
|
||||
err := ns.sendWebRequestSync(context.Background(), webhook)
|
||||
|
||||
if err != nil {
|
||||
ns.log.Error("Failed to send webrequest ", "error", err)
|
||||
}
|
||||
case msg := <-ns.mailQueue:
|
||||
num, err := ns.send(msg)
|
||||
tos := strings.Join(msg.To, "; ")
|
||||
info := ""
|
||||
if err != nil {
|
||||
if len(msg.Info) > 0 {
|
||||
info = ", info: " + msg.Info
|
||||
}
|
||||
ns.log.Error(fmt.Sprintf("Async sent email %d succeed, not send emails: %s%s err: %s", num, tos, info, err))
|
||||
} else {
|
||||
ns.log.Debug(fmt.Sprintf("Async sent email %d succeed, sent emails: %s%s", num, tos, info))
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
|
||||
return ns.sendWebRequestSync(ctx, &Webhook{
|
||||
Url: cmd.Url,
|
||||
User: cmd.User,
|
||||
Password: cmd.Password,
|
||||
Body: cmd.Body,
|
||||
HttpMethod: cmd.HttpMethod,
|
||||
HttpHeader: cmd.HttpHeader,
|
||||
ContentType: cmd.ContentType,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -74,8 +119,8 @@ func subjectTemplateFunc(obj map[string]interface{}, value string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSync) error {
|
||||
message, err := buildEmailMessage(&m.SendEmailCommand{
|
||||
func (ns *NotificationService) sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSync) error {
|
||||
message, err := ns.buildEmailMessage(&m.SendEmailCommand{
|
||||
Data: cmd.Data,
|
||||
Info: cmd.Info,
|
||||
Template: cmd.Template,
|
||||
@@ -88,25 +133,23 @@ func sendEmailCommandHandlerSync(ctx context.Context, cmd *m.SendEmailCommandSyn
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = send(message)
|
||||
|
||||
_, err = ns.send(message)
|
||||
return err
|
||||
}
|
||||
|
||||
func sendEmailCommandHandler(cmd *m.SendEmailCommand) error {
|
||||
message, err := buildEmailMessage(cmd)
|
||||
func (ns *NotificationService) sendEmailCommandHandler(cmd *m.SendEmailCommand) error {
|
||||
message, err := ns.buildEmailMessage(cmd)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addToMailQueue(message)
|
||||
|
||||
ns.mailQueue <- message
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error {
|
||||
return sendEmailCommandHandler(&m.SendEmailCommand{
|
||||
func (ns *NotificationService) sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error {
|
||||
return ns.sendEmailCommandHandler(&m.SendEmailCommand{
|
||||
To: []string{cmd.User.Email},
|
||||
Template: tmplResetPassword,
|
||||
Data: map[string]interface{}{
|
||||
@@ -116,7 +159,7 @@ func sendResetPasswordEmail(cmd *m.SendResetPasswordEmailCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
func validateResetPasswordCode(query *m.ValidateResetPasswordCodeQuery) error {
|
||||
func (ns *NotificationService) validateResetPasswordCode(query *m.ValidateResetPasswordCodeQuery) error {
|
||||
login := getLoginForEmailCode(query.Code)
|
||||
if login == "" {
|
||||
return m.ErrInvalidEmailCode
|
||||
@@ -135,18 +178,18 @@ func validateResetPasswordCode(query *m.ValidateResetPasswordCodeQuery) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func signUpStartedHandler(evt *events.SignUpStarted) error {
|
||||
func (ns *NotificationService) signUpStartedHandler(evt *events.SignUpStarted) error {
|
||||
if !setting.VerifyEmailEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("User signup started: %s", evt.Email)
|
||||
ns.log.Info("User signup started", "email", evt.Email)
|
||||
|
||||
if evt.Email == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := sendEmailCommandHandler(&m.SendEmailCommand{
|
||||
err := ns.sendEmailCommandHandler(&m.SendEmailCommand{
|
||||
To: []string{evt.Email},
|
||||
Template: tmplSignUpStarted,
|
||||
Data: map[string]interface{}{
|
||||
@@ -155,6 +198,7 @@ func signUpStartedHandler(evt *events.SignUpStarted) error {
|
||||
"SignUpUrl": setting.ToAbsUrl(fmt.Sprintf("signup/?email=%s&code=%s", url.QueryEscape(evt.Email), url.QueryEscape(evt.Code))),
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -163,12 +207,12 @@ func signUpStartedHandler(evt *events.SignUpStarted) error {
|
||||
return bus.Dispatch(&emailSentCmd)
|
||||
}
|
||||
|
||||
func signUpCompletedHandler(evt *events.SignUpCompleted) error {
|
||||
if evt.Email == "" || !setting.Smtp.SendWelcomeEmailOnSignUp {
|
||||
func (ns *NotificationService) signUpCompletedHandler(evt *events.SignUpCompleted) error {
|
||||
if evt.Email == "" || !ns.Cfg.Smtp.SendWelcomeEmailOnSignUp {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sendEmailCommandHandler(&m.SendEmailCommand{
|
||||
return ns.sendEmailCommandHandler(&m.SendEmailCommand{
|
||||
To: []string{evt.Email},
|
||||
Template: tmplWelcomeOnSignUp,
|
||||
Data: map[string]interface{}{
|
||||
|
||||
@@ -3,6 +3,7 @@ package notifications
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
@@ -17,25 +18,24 @@ type testTriggeredAlert struct {
|
||||
func TestNotifications(t *testing.T) {
|
||||
|
||||
Convey("Given the notifications service", t, func() {
|
||||
//bus.ClearBusHandlers()
|
||||
|
||||
setting.StaticRootPath = "../../../public/"
|
||||
setting.Smtp.Enabled = true
|
||||
setting.Smtp.TemplatesPattern = "emails/*.html"
|
||||
setting.Smtp.FromAddress = "from@address.com"
|
||||
setting.Smtp.FromName = "Grafana Admin"
|
||||
|
||||
err := Init()
|
||||
ns := &NotificationService{}
|
||||
ns.Bus = bus.New()
|
||||
ns.Cfg = setting.NewCfg()
|
||||
ns.Cfg.Smtp.Enabled = true
|
||||
ns.Cfg.Smtp.TemplatesPattern = "emails/*.html"
|
||||
ns.Cfg.Smtp.FromAddress = "from@address.com"
|
||||
ns.Cfg.Smtp.FromName = "Grafana Admin"
|
||||
|
||||
err := ns.Init()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
var sentMsg *Message
|
||||
addToMailQueue = func(msg *Message) {
|
||||
sentMsg = msg
|
||||
}
|
||||
|
||||
Convey("When sending reset email password", func() {
|
||||
err := sendResetPasswordEmail(&m.SendResetPasswordEmailCommand{User: &m.User{Email: "asd@asd.com"}})
|
||||
err := ns.sendResetPasswordEmail(&m.SendResetPasswordEmailCommand{User: &m.User{Email: "asd@asd.com"}})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
sentMsg := <-ns.mailQueue
|
||||
So(sentMsg.Body, ShouldContainSubstring, "body")
|
||||
So(sentMsg.Subject, ShouldEqual, "Reset your Grafana password - asd@asd.com")
|
||||
So(sentMsg.Body, ShouldNotContainSubstring, "Subject")
|
||||
|
||||
@@ -12,23 +12,19 @@ import (
|
||||
|
||||
func TestEmailIntegrationTest(t *testing.T) {
|
||||
SkipConvey("Given the notifications service", t, func() {
|
||||
bus.ClearBusHandlers()
|
||||
|
||||
setting.StaticRootPath = "../../../public/"
|
||||
setting.Smtp.Enabled = true
|
||||
setting.Smtp.TemplatesPattern = "emails/*.html"
|
||||
setting.Smtp.FromAddress = "from@address.com"
|
||||
setting.Smtp.FromName = "Grafana Admin"
|
||||
setting.BuildVersion = "4.0.0"
|
||||
|
||||
err := Init()
|
||||
So(err, ShouldBeNil)
|
||||
ns := &NotificationService{}
|
||||
ns.Bus = bus.New()
|
||||
ns.Cfg = setting.NewCfg()
|
||||
ns.Cfg.Smtp.Enabled = true
|
||||
ns.Cfg.Smtp.TemplatesPattern = "emails/*.html"
|
||||
ns.Cfg.Smtp.FromAddress = "from@address.com"
|
||||
ns.Cfg.Smtp.FromName = "Grafana Admin"
|
||||
|
||||
addToMailQueue = func(msg *Message) {
|
||||
So(msg.From, ShouldEqual, "Grafana Admin <from@address.com>")
|
||||
So(msg.To[0], ShouldEqual, "asdf@asdf.com")
|
||||
ioutil.WriteFile("../../../tmp/test_email.html", []byte(msg.Body), 0777)
|
||||
}
|
||||
err := ns.Init()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("When sending reset email password", func() {
|
||||
cmd := &m.SendEmailCommand{
|
||||
@@ -59,8 +55,13 @@ func TestEmailIntegrationTest(t *testing.T) {
|
||||
Template: "alert_notification.html",
|
||||
}
|
||||
|
||||
err := sendEmailCommandHandler(cmd)
|
||||
err := ns.sendEmailCommandHandler(cmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
sentMsg := <-ns.mailQueue
|
||||
So(sentMsg.From, ShouldEqual, "Grafana Admin <from@address.com>")
|
||||
So(sentMsg.To[0], ShouldEqual, "asdf@asdf.com")
|
||||
ioutil.WriteFile("../../../tmp/test_email.html", []byte(sentMsg.Body), 0777)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,17 +11,17 @@ import (
|
||||
|
||||
"golang.org/x/net/context/ctxhttp"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
type Webhook struct {
|
||||
Url string
|
||||
User string
|
||||
Password string
|
||||
Body string
|
||||
HttpMethod string
|
||||
HttpHeader map[string]string
|
||||
Url string
|
||||
User string
|
||||
Password string
|
||||
Body string
|
||||
HttpMethod string
|
||||
HttpHeader map[string]string
|
||||
ContentType string
|
||||
}
|
||||
|
||||
var netTransport = &http.Transport{
|
||||
@@ -37,32 +37,8 @@ var netClient = &http.Client{
|
||||
Transport: netTransport,
|
||||
}
|
||||
|
||||
var (
|
||||
webhookQueue chan *Webhook
|
||||
webhookLog log.Logger
|
||||
)
|
||||
|
||||
func initWebhookQueue() {
|
||||
webhookLog = log.New("notifications.webhook")
|
||||
webhookQueue = make(chan *Webhook, 10)
|
||||
go processWebhookQueue()
|
||||
}
|
||||
|
||||
func processWebhookQueue() {
|
||||
for {
|
||||
select {
|
||||
case webhook := <-webhookQueue:
|
||||
err := sendWebRequestSync(context.Background(), webhook)
|
||||
|
||||
if err != nil {
|
||||
webhookLog.Error("Failed to send webrequest ", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
|
||||
webhookLog.Debug("Sending webhook", "url", webhook.Url, "http method", webhook.HttpMethod)
|
||||
func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
|
||||
ns.log.Debug("Sending webhook", "url", webhook.Url, "http method", webhook.HttpMethod)
|
||||
|
||||
if webhook.HttpMethod == "" {
|
||||
webhook.HttpMethod = http.MethodPost
|
||||
@@ -73,8 +49,13 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
|
||||
return err
|
||||
}
|
||||
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
if webhook.ContentType == "" {
|
||||
webhook.ContentType = "application/json"
|
||||
}
|
||||
|
||||
request.Header.Add("Content-Type", webhook.ContentType)
|
||||
request.Header.Add("User-Agent", "Grafana")
|
||||
|
||||
if webhook.User != "" && webhook.Password != "" {
|
||||
request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password))
|
||||
}
|
||||
@@ -98,10 +79,6 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
|
||||
return err
|
||||
}
|
||||
|
||||
webhookLog.Debug("Webhook failed", "statuscode", resp.Status, "body", string(body))
|
||||
ns.log.Debug("Webhook failed", "statuscode", resp.Status, "body", string(body))
|
||||
return fmt.Errorf("Webhook response status %v", resp.Status)
|
||||
}
|
||||
|
||||
var addToWebhookQueue = func(msg *Webhook) {
|
||||
webhookQueue <- msg
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
|
||||
|
||||
files, err := ioutil.ReadDir(cr.path)
|
||||
if err != nil {
|
||||
cr.log.Error("cant read dashboard provisioning files from directory", "path", cr.path)
|
||||
cr.log.Error("can't read dashboard provisioning files from directory", "path", cr.path)
|
||||
return dashboards, nil
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
|
||||
|
||||
parsedDashboards, err := cr.parseConfigs(file)
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(parsedDashboards) > 0 {
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
simpleDashboardConfig string = "./test-configs/dashboards-from-disk"
|
||||
oldVersion string = "./test-configs/version-0"
|
||||
brokenConfigs string = "./test-configs/broken-configs"
|
||||
simpleDashboardConfig = "./test-configs/dashboards-from-disk"
|
||||
oldVersion = "./test-configs/version-0"
|
||||
brokenConfigs = "./test-configs/broken-configs"
|
||||
)
|
||||
|
||||
func TestDashboardsAsConfig(t *testing.T) {
|
||||
|
||||
@@ -10,19 +10,16 @@ import (
|
||||
type DashboardProvisioner struct {
|
||||
cfgReader *configReader
|
||||
log log.Logger
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func Provision(ctx context.Context, configDirectory string) (*DashboardProvisioner, error) {
|
||||
func NewDashboardProvisioner(configDirectory string) *DashboardProvisioner {
|
||||
log := log.New("provisioning.dashboard")
|
||||
d := &DashboardProvisioner{
|
||||
cfgReader: &configReader{path: configDirectory, log: log},
|
||||
log: log,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
err := d.Provision(ctx)
|
||||
return d, err
|
||||
return d
|
||||
}
|
||||
|
||||
func (provider *DashboardProvisioner) Provision(ctx context.Context) error {
|
||||
|
||||
@@ -19,9 +19,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
checkDiskForChangesInterval time.Duration = time.Second * 3
|
||||
checkDiskForChangesInterval = time.Second * 3
|
||||
|
||||
ErrFolderNameMissing error = errors.New("Folder name missing")
|
||||
ErrFolderNameMissing = errors.New("Folder name missing")
|
||||
)
|
||||
|
||||
type fileReader struct {
|
||||
@@ -235,7 +235,6 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, service dashboards.DashboardPr
|
||||
func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) {
|
||||
checkFilepath, err := filepath.EvalSymlinks(path)
|
||||
if path != checkFilepath {
|
||||
path = checkFilepath
|
||||
fi, err := os.Lstat(checkFilepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -55,9 +55,6 @@ func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *Das
|
||||
dash.OrgId = cfg.OrgId
|
||||
dash.Dashboard.OrgId = cfg.OrgId
|
||||
dash.Dashboard.FolderId = folderId
|
||||
if !cfg.Editable {
|
||||
dash.Dashboard.Data.Set("editable", cfg.Editable)
|
||||
}
|
||||
|
||||
if dash.Dashboard.Title == "" {
|
||||
return nil, models.ErrDashboardTitleEmpty
|
||||
|
||||
@@ -19,7 +19,7 @@ func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error)
|
||||
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
cr.log.Error("cant read datasource provisioning files from directory", "path", path)
|
||||
cr.log.Error("can't read datasource provisioning files from directory", "path", path)
|
||||
return datasources, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
logger log.Logger = log.New("fake.log")
|
||||
oneDatasourcesConfig string = ""
|
||||
twoDatasourcesConfig string = "./test-configs/two-datasources"
|
||||
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"
|
||||
logger log.Logger = log.New("fake.log")
|
||||
|
||||
twoDatasourcesConfig = "./test-configs/two-datasources"
|
||||
twoDatasourcesConfigPurgeOthers = "./test-configs/insert-two-delete-two"
|
||||
doubleDatasourcesConfig = "./test-configs/double-default"
|
||||
allProperties = "./test-configs/all-properties"
|
||||
versionZero = "./test-configs/version-0"
|
||||
brokenYaml = "./test-configs/broken-yaml"
|
||||
|
||||
fakeRepo *fakeRepository
|
||||
)
|
||||
|
||||
@@ -2,34 +2,40 @@ package provisioning
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
|
||||
ini "gopkg.in/ini.v1"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func Init(ctx context.Context, homePath string, cfg *ini.File) error {
|
||||
provisioningPath := makeAbsolute(cfg.Section("paths").Key("provisioning").String(), homePath)
|
||||
func init() {
|
||||
registry.RegisterService(&ProvisioningService{})
|
||||
}
|
||||
|
||||
datasourcePath := path.Join(provisioningPath, "datasources")
|
||||
type ProvisioningService struct {
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
}
|
||||
|
||||
func (ps *ProvisioningService) Init() error {
|
||||
datasourcePath := path.Join(ps.Cfg.ProvisioningPath, "datasources")
|
||||
if err := datasources.Provision(datasourcePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dashboardPath := path.Join(provisioningPath, "dashboards")
|
||||
_, err := dashboards.Provision(ctx, dashboardPath)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("Datasource provisioning error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeAbsolute(path string, root string) string {
|
||||
if filepath.IsAbs(path) {
|
||||
return path
|
||||
func (ps *ProvisioningService) Run(ctx context.Context) error {
|
||||
dashboardPath := path.Join(ps.Cfg.ProvisioningPath, "dashboards")
|
||||
dashProvisioner := dashboards.NewDashboardProvisioner(dashboardPath)
|
||||
|
||||
if err := dashProvisioner.Provision(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return filepath.Join(root, path)
|
||||
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
@@ -5,13 +5,23 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
bus.AddHandler("search", searchHandler)
|
||||
func init() {
|
||||
registry.RegisterService(&SearchService{})
|
||||
}
|
||||
|
||||
func searchHandler(query *Query) error {
|
||||
type SearchService struct {
|
||||
Bus bus.Bus `inject:""`
|
||||
}
|
||||
|
||||
func (s *SearchService) Init() error {
|
||||
s.Bus.AddHandler(s.searchHandler)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SearchService) searchHandler(query *Query) error {
|
||||
dashQuery := FindPersistedDashboardsQuery{
|
||||
Title: query.Title,
|
||||
SignedInUser: query.SignedInUser,
|
||||
|
||||
@@ -12,6 +12,7 @@ func TestSearch(t *testing.T) {
|
||||
|
||||
Convey("Given search query", t, func() {
|
||||
query := Query{Limit: 2000, SignedInUser: &m.SignedInUser{IsGrafanaAdmin: true}}
|
||||
ss := &SearchService{}
|
||||
|
||||
bus.AddHandler("test", func(query *FindPersistedDashboardsQuery) error {
|
||||
query.Result = HitList{
|
||||
@@ -35,7 +36,7 @@ func TestSearch(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("That is empty", func() {
|
||||
err := searchHandler(&query)
|
||||
err := ss.searchHandler(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("should return sorted results", func() {
|
||||
|
||||
@@ -23,12 +23,7 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
sql := "DELETE FROM alert_notification WHERE alert_notification.org_id = ? AND alert_notification.id = ?"
|
||||
_, err := sess.Exec(sql, cmd.OrgId, cmd.Id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
}
|
||||
|
||||
err := GetAlertNotifications(cmd)
|
||||
fmt.Printf("errror %v", err)
|
||||
fmt.Printf("error %v", err)
|
||||
So(err, ShouldBeNil)
|
||||
So(cmd.Result, ShouldBeNil)
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
@@ -17,18 +18,24 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
tags := models.ParseTagPairs(item.Tags)
|
||||
item.Tags = models.JoinTagPairs(tags)
|
||||
item.Created = time.Now().UnixNano() / int64(time.Millisecond)
|
||||
item.Updated = item.Created
|
||||
if item.Epoch == 0 {
|
||||
item.Epoch = item.Created
|
||||
}
|
||||
|
||||
if _, err := sess.Table("annotation").Insert(item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if item.Tags != nil {
|
||||
if tags, err := r.ensureTagsExist(sess, tags); err != nil {
|
||||
tags, err := r.ensureTagsExist(sess, tags)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
for _, tag := range tags {
|
||||
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", item.Id, tag.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", item.Id, tag.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,6 +86,7 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
|
||||
return errors.New("Annotation not found")
|
||||
}
|
||||
|
||||
existing.Updated = time.Now().UnixNano() / int64(time.Millisecond)
|
||||
existing.Epoch = item.Epoch
|
||||
existing.Text = item.Text
|
||||
if item.RegionId != 0 {
|
||||
@@ -86,27 +94,24 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
|
||||
}
|
||||
|
||||
if item.Tags != nil {
|
||||
if tags, err := r.ensureTagsExist(sess, models.ParseTagPairs(item.Tags)); err != nil {
|
||||
tags, err := r.ensureTagsExist(sess, models.ParseTagPairs(item.Tags))
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
if _, err := sess.Exec("DELETE FROM annotation_tag WHERE annotation_id = ?", existing.Id); err != nil {
|
||||
}
|
||||
if _, err := sess.Exec("DELETE FROM annotation_tag WHERE annotation_id = ?", existing.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", existing.Id, tag.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", existing.Id, tag.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
existing.Tags = item.Tags
|
||||
|
||||
if _, err := sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err = sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "updated", "tags").Update(existing)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -127,6 +132,8 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
||||
annotation.text,
|
||||
annotation.tags,
|
||||
annotation.data,
|
||||
annotation.created,
|
||||
annotation.updated,
|
||||
usr.email,
|
||||
usr.login,
|
||||
alert.name as alert_name
|
||||
@@ -164,6 +171,11 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
||||
params = append(params, query.PanelId)
|
||||
}
|
||||
|
||||
if query.UserId != 0 {
|
||||
sql.WriteString(` AND annotation.user_id = ?`)
|
||||
params = append(params, query.UserId)
|
||||
}
|
||||
|
||||
if query.From > 0 && query.To > 0 {
|
||||
sql.WriteString(` AND annotation.epoch BETWEEN ? AND ?`)
|
||||
params = append(params, query.From, query.To)
|
||||
@@ -171,6 +183,8 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
||||
|
||||
if query.Type == "alert" {
|
||||
sql.WriteString(` AND annotation.alert_id > 0`)
|
||||
} else if query.Type == "annotation" {
|
||||
sql.WriteString(` AND annotation.alert_id = 0`)
|
||||
}
|
||||
|
||||
if len(query.Tags) > 0 {
|
||||
@@ -202,7 +216,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
||||
}
|
||||
|
||||
if query.Limit == 0 {
|
||||
query.Limit = 10
|
||||
query.Limit = 100
|
||||
}
|
||||
|
||||
sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
|
||||
|
||||
@@ -79,6 +79,12 @@ func TestAnnotations(t *testing.T) {
|
||||
Convey("Can read tags", func() {
|
||||
So(items[0].Tags, ShouldResemble, []string{"outage", "error", "type:outage", "server:server-1"})
|
||||
})
|
||||
|
||||
Convey("Has created and updated values", func() {
|
||||
So(items[0].Created, ShouldBeGreaterThan, 0)
|
||||
So(items[0].Updated, ShouldBeGreaterThan, 0)
|
||||
So(items[0].Updated, ShouldEqual, items[0].Created)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can query for annotation by id", func() {
|
||||
@@ -231,6 +237,10 @@ func TestAnnotations(t *testing.T) {
|
||||
So(items[0].Tags, ShouldResemble, []string{"newtag1", "newtag2"})
|
||||
So(items[0].Text, ShouldEqual, "something new")
|
||||
})
|
||||
|
||||
Convey("Updated time has increased", func() {
|
||||
So(items[0].Updated, ShouldBeGreaterThan, items[0].Created)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can delete annotation", func() {
|
||||
@@ -246,6 +256,7 @@ func TestAnnotations(t *testing.T) {
|
||||
annotationId := items[0].Id
|
||||
|
||||
err = repo.Delete(&annotations.DeleteParams{Id: annotationId})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
items, err = repo.Find(query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
@@ -55,7 +55,7 @@ func GetApiKeyById(query *m.GetApiKeyByIdQuery) error {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
} else if !has {
|
||||
return m.ErrInvalidApiKey
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func GetApiKeyByName(query *m.GetApiKeyByNameQuery) error {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
} else if !has {
|
||||
return m.ErrInvalidApiKey
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ func init() {
|
||||
bus.AddHandler("sql", GetDashboardPermissionsForUser)
|
||||
bus.AddHandler("sql", GetDashboardsBySlug)
|
||||
bus.AddHandler("sql", ValidateDashboardBeforeSave)
|
||||
bus.AddHandler("sql", HasEditPermissionInFolders)
|
||||
}
|
||||
|
||||
var generateNewUid func() string = util.GenerateShortUid
|
||||
@@ -63,7 +64,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
|
||||
}
|
||||
|
||||
// do not allow plugin dashboard updates without overwrite flag
|
||||
if existing.PluginId != "" && cmd.Overwrite == false {
|
||||
if existing.PluginId != "" && !cmd.Overwrite {
|
||||
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +78,7 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error {
|
||||
}
|
||||
|
||||
parentVersion := dash.Version
|
||||
affectedRows := int64(0)
|
||||
var affectedRows int64
|
||||
var err error
|
||||
|
||||
if dash.Id == 0 {
|
||||
@@ -172,7 +173,7 @@ func GetDashboard(query *m.GetDashboardQuery) error {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
} else if !has {
|
||||
return m.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
@@ -308,7 +309,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
|
||||
has, err := sess.Get(&dashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
} else if !has {
|
||||
return m.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
@@ -347,12 +348,7 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
|
||||
|
||||
err := x.In("id", query.DashboardIds).Find(&dashboards)
|
||||
query.Result = dashboards
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDashboardPermissionsForUser returns the maximum permission the specified user has for a dashboard(s)
|
||||
@@ -431,12 +427,7 @@ func GetDashboardsByPluginId(query *m.GetDashboardsByPluginIdQuery) error {
|
||||
|
||||
err := x.Where(whereExpr, query.OrgId, query.PluginId).Find(&dashboards)
|
||||
query.Result = dashboards
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
type DashboardSlugDTO struct {
|
||||
@@ -451,7 +442,7 @@ func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if exists == false {
|
||||
} else if !exists {
|
||||
return m.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
@@ -479,7 +470,7 @@ func GetDashboardUIDById(query *m.GetDashboardRefByIdQuery) error {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if exists == false {
|
||||
} else if !exists {
|
||||
return m.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
@@ -544,6 +535,10 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
|
||||
dash.SetId(existingByUid.Id)
|
||||
dash.SetUid(existingByUid.Uid)
|
||||
existing = existingByUid
|
||||
|
||||
if !dash.IsFolder {
|
||||
cmd.Result.IsParentFolderChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
if (existing.IsFolder && !dash.IsFolder) ||
|
||||
@@ -551,6 +546,10 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
|
||||
return m.ErrDashboardTypeMismatch
|
||||
}
|
||||
|
||||
if !dash.IsFolder && dash.FolderId != existing.FolderId {
|
||||
cmd.Result.IsParentFolderChanged = true
|
||||
}
|
||||
|
||||
// check for is someone else has written in between
|
||||
if dash.Version != existing.Version {
|
||||
if cmd.Overwrite {
|
||||
@@ -561,7 +560,7 @@ func getExistingDashboardByIdOrUidForUpdate(sess *DBSession, cmd *m.ValidateDash
|
||||
}
|
||||
|
||||
// do not allow plugin dashboard updates without overwrite flag
|
||||
if existing.PluginId != "" && cmd.Overwrite == false {
|
||||
if existing.PluginId != "" && !cmd.Overwrite {
|
||||
return m.UpdatePluginDashboardError{PluginId: existing.PluginId}
|
||||
}
|
||||
|
||||
@@ -586,6 +585,10 @@ func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashbo
|
||||
return m.ErrDashboardFolderWithSameNameAsDashboard
|
||||
}
|
||||
|
||||
if !dash.IsFolder && (dash.FolderId != existing.FolderId || dash.Id == 0) {
|
||||
cmd.Result.IsParentFolderChanged = true
|
||||
}
|
||||
|
||||
if cmd.Overwrite {
|
||||
dash.SetId(existing.Id)
|
||||
dash.SetUid(existing.Uid)
|
||||
@@ -599,6 +602,7 @@ func getExistingDashboardByTitleAndFolder(sess *DBSession, cmd *m.ValidateDashbo
|
||||
}
|
||||
|
||||
func ValidateDashboardBeforeSave(cmd *m.ValidateDashboardBeforeSaveCommand) (err error) {
|
||||
cmd.Result = &m.ValidateDashboardBeforeSaveResult{}
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
if err = getExistingDashboardByIdOrUidForUpdate(sess, cmd); err != nil {
|
||||
return err
|
||||
@@ -611,3 +615,27 @@ func ValidateDashboardBeforeSave(cmd *m.ValidateDashboardBeforeSaveCommand) (err
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func HasEditPermissionInFolders(query *m.HasEditPermissionInFoldersQuery) error {
|
||||
if query.SignedInUser.HasRole(m.ROLE_EDITOR) {
|
||||
query.Result = true
|
||||
return nil
|
||||
}
|
||||
|
||||
builder := &SqlBuilder{}
|
||||
builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?", query.SignedInUser.OrgId, dialect.BooleanStr(true))
|
||||
builder.writeDashboardPermissionFilter(query.SignedInUser, m.PERMISSION_EDIT)
|
||||
|
||||
type folderCount struct {
|
||||
Count int64
|
||||
}
|
||||
|
||||
resp := make([]*folderCount, 0)
|
||||
if err := x.Sql(builder.GetSqlString(), builder.params...).Find(&resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query.Result = len(resp) > 0 && resp[0].Count > 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -35,10 +35,8 @@ func UpdateDashboardAcl(cmd *m.UpdateDashboardAclCommand) error {
|
||||
|
||||
// Update dashboard HasAcl flag
|
||||
dashboard := m.Dashboard{HasAcl: true}
|
||||
if _, err := sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
_, err = sess.Cols("has_acl").Where("id=?", cmd.DashboardId).Update(&dashboard)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,7 +67,8 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
|
||||
'' as title,
|
||||
'' as slug,
|
||||
'' as uid,` +
|
||||
falseStr + ` AS is_folder
|
||||
falseStr + ` AS is_folder,` +
|
||||
falseStr + ` AS inherited
|
||||
FROM dashboard_acl as da
|
||||
WHERE da.dashboard_id = -1`
|
||||
query.Result = make([]*m.DashboardAclInfoDTO, 0)
|
||||
@@ -92,10 +91,12 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
|
||||
u.login AS user_login,
|
||||
u.email AS user_email,
|
||||
ug.name AS team,
|
||||
ug.email AS team_email,
|
||||
d.title,
|
||||
d.slug,
|
||||
d.uid,
|
||||
d.is_folder
|
||||
d.is_folder,
|
||||
CASE WHEN (da.dashboard_id = -1 AND d.folder_id > 0) OR da.dashboard_id = d.folder_id THEN ` + dialect.BooleanStr(true) + ` ELSE ` + falseStr + ` END AS inherited
|
||||
FROM dashboard as d
|
||||
LEFT JOIN dashboard folder on folder.id = d.folder_id
|
||||
LEFT JOIN dashboard_acl AS da ON
|
||||
|
||||
@@ -26,6 +26,22 @@ func TestDashboardAclDataAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Given dashboard folder with default permissions", func() {
|
||||
Convey("When reading folder acl should include default acl", func() {
|
||||
query := m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
|
||||
|
||||
err := GetDashboardAclInfoList(&query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
defaultPermissionsId := -1
|
||||
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
|
||||
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
|
||||
So(query.Result[0].Inherited, ShouldBeFalse)
|
||||
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
|
||||
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
|
||||
So(query.Result[1].Inherited, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("When reading dashboard acl should include acl for parent folder", func() {
|
||||
query := m.GetDashboardAclInfoListQuery{DashboardId: childDash.Id, OrgId: 1}
|
||||
|
||||
@@ -36,8 +52,10 @@ func TestDashboardAclDataAccess(t *testing.T) {
|
||||
defaultPermissionsId := -1
|
||||
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
|
||||
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
|
||||
So(query.Result[0].Inherited, ShouldBeTrue)
|
||||
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
|
||||
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
|
||||
So(query.Result[1].Inherited, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -94,7 +112,9 @@ func TestDashboardAclDataAccess(t *testing.T) {
|
||||
|
||||
So(len(query.Result), ShouldEqual, 2)
|
||||
So(query.Result[0].DashboardId, ShouldEqual, savedFolder.Id)
|
||||
So(query.Result[0].Inherited, ShouldBeTrue)
|
||||
So(query.Result[1].DashboardId, ShouldEqual, childDash.Id)
|
||||
So(query.Result[1].Inherited, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -118,9 +138,12 @@ func TestDashboardAclDataAccess(t *testing.T) {
|
||||
So(len(query.Result), ShouldEqual, 3)
|
||||
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
|
||||
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
|
||||
So(query.Result[0].Inherited, ShouldBeTrue)
|
||||
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
|
||||
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
|
||||
So(query.Result[1].Inherited, ShouldBeTrue)
|
||||
So(query.Result[2].DashboardId, ShouldEqual, childDash.Id)
|
||||
So(query.Result[2].Inherited, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -131,6 +154,7 @@ func TestDashboardAclDataAccess(t *testing.T) {
|
||||
DashboardId: savedFolder.Id,
|
||||
Permission: m.PERMISSION_EDIT,
|
||||
})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
q1 := &m.GetDashboardAclInfoListQuery{DashboardId: savedFolder.Id, OrgId: 1}
|
||||
err = GetDashboardAclInfoList(q1)
|
||||
@@ -209,8 +233,10 @@ func TestDashboardAclDataAccess(t *testing.T) {
|
||||
defaultPermissionsId := -1
|
||||
So(query.Result[0].DashboardId, ShouldEqual, defaultPermissionsId)
|
||||
So(*query.Result[0].Role, ShouldEqual, m.ROLE_VIEWER)
|
||||
So(query.Result[0].Inherited, ShouldBeFalse)
|
||||
So(query.Result[1].DashboardId, ShouldEqual, defaultPermissionsId)
|
||||
So(*query.Result[1].Role, ShouldEqual, m.ROLE_EDITOR)
|
||||
So(query.Result[1].Inherited, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -221,7 +221,6 @@ func TestDashboardFolderDataAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Given two dashboard folders", func() {
|
||||
|
||||
folder1 := insertTestDashboard("1 test dash folder", 1, 0, true, "prod")
|
||||
folder2 := insertTestDashboard("2 test dash folder", 1, 0, true, "prod")
|
||||
insertTestDashboard("folder in another org", 2, 0, true, "prod")
|
||||
@@ -264,6 +263,15 @@ func TestDashboardFolderDataAccess(t *testing.T) {
|
||||
So(query.Result[1].DashboardId, ShouldEqual, folder2.Id)
|
||||
So(query.Result[1].Permission, ShouldEqual, m.PERMISSION_ADMIN)
|
||||
})
|
||||
|
||||
Convey("should have edit permission in folders", func() {
|
||||
query := &m.HasEditPermissionInFoldersQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgId: 1, OrgRole: m.ROLE_ADMIN},
|
||||
}
|
||||
err := HasEditPermissionInFolders(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Editor users", func() {
|
||||
@@ -310,6 +318,14 @@ func TestDashboardFolderDataAccess(t *testing.T) {
|
||||
So(query.Result[0].Id, ShouldEqual, folder2.Id)
|
||||
})
|
||||
|
||||
Convey("should have edit permission in folders", func() {
|
||||
query := &m.HasEditPermissionInFoldersQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgId: 1, OrgRole: m.ROLE_EDITOR},
|
||||
}
|
||||
err := HasEditPermissionInFolders(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Viewer users", func() {
|
||||
@@ -353,6 +369,41 @@ func TestDashboardFolderDataAccess(t *testing.T) {
|
||||
So(len(query.Result), ShouldEqual, 1)
|
||||
So(query.Result[0].Id, ShouldEqual, folder1.Id)
|
||||
})
|
||||
|
||||
Convey("should not have edit permission in folders", func() {
|
||||
query := &m.HasEditPermissionInFoldersQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
|
||||
}
|
||||
err := HasEditPermissionInFolders(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("and admin permission is given for user with org role viewer in one dashboard folder", func() {
|
||||
testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: m.PERMISSION_ADMIN})
|
||||
|
||||
Convey("should have edit permission in folders", func() {
|
||||
query := &m.HasEditPermissionInFoldersQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
|
||||
}
|
||||
err := HasEditPermissionInFolders(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("and edit permission is given for user with org role viewer in one dashboard folder", func() {
|
||||
testHelperUpdateDashboardAcl(folder1.Id, m.DashboardAcl{DashboardId: folder1.Id, OrgId: 1, UserId: viewerUser.Id, Permission: m.PERMISSION_EDIT})
|
||||
|
||||
Convey("should have edit permission in folders", func() {
|
||||
query := &m.HasEditPermissionInFoldersQuery{
|
||||
SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgId: 1, OrgRole: m.ROLE_VIEWER},
|
||||
}
|
||||
err := HasEditPermissionInFolders(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
func init() {
|
||||
bus.AddHandler("sql", GetProvisionedDashboardDataQuery)
|
||||
bus.AddHandler("sql", SaveProvisionedDashboard)
|
||||
bus.AddHandler("sql", GetProvisionedDataByDashboardId)
|
||||
}
|
||||
|
||||
type DashboardExtras struct {
|
||||
@@ -17,6 +18,19 @@ type DashboardExtras struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func GetProvisionedDataByDashboardId(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
result := &models.DashboardProvisioning{}
|
||||
|
||||
exist, err := x.Where("dashboard_id = ?", cmd.DashboardId).Get(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Result = exist
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
err := saveDashboard(sess, cmd.DashboardCmd)
|
||||
|
||||
@@ -50,6 +50,23 @@ func TestDashboardProvisioningTest(t *testing.T) {
|
||||
So(query.Result[0].DashboardId, ShouldEqual, dashId)
|
||||
So(query.Result[0].Updated, ShouldEqual, now.Unix())
|
||||
})
|
||||
|
||||
Convey("Can query for one provisioned dashboard", func() {
|
||||
query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id}
|
||||
|
||||
err := GetProvisionedDataByDashboardId(query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(query.Result, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Can query for none provisioned dashboard", func() {
|
||||
query := &models.IsDashboardProvisionedQuery{DashboardId: 3000}
|
||||
|
||||
err := GetProvisionedDataByDashboardId(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ func TestIntegratedDashboardService(t *testing.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
|
||||
})
|
||||
@@ -28,6 +27,11 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error {
|
||||
cmd.Result = false
|
||||
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)
|
||||
@@ -74,7 +78,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
Convey("Given organization B", func() {
|
||||
var otherOrgId int64 = 2
|
||||
|
||||
Convey("When saving a dashboard with id that are saved in organization A", func() {
|
||||
Convey("When creating a dashboard with same id as dashboard in organization A", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: otherOrgId,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
@@ -93,7 +97,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
})
|
||||
|
||||
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() {
|
||||
Convey("When creating a dashboard with same uid as dashboard in organization A", func() {
|
||||
var otherOrgId int64 = 2
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: otherOrgId,
|
||||
@@ -106,7 +110,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
|
||||
res := callSaveWithResult(cmd)
|
||||
|
||||
Convey("It should create dashboard in other organization", func() {
|
||||
Convey("It should create a new dashboard in organization B", func() {
|
||||
So(res, ShouldNotBeNil)
|
||||
|
||||
query := models.GetDashboardQuery{OrgId: otherOrgId, Uid: savedDashInFolder.Uid}
|
||||
@@ -126,7 +130,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
|
||||
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() {
|
||||
Convey("When creating a new dashboard in the General folder", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: testOrgId,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
@@ -138,7 +142,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
|
||||
err := callSaveWithError(cmd)
|
||||
|
||||
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
|
||||
Convey("It should create dashboard guardian for General Folder with correct arguments and result in access denied error", func() {
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||
|
||||
@@ -148,7 +152,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When trying to create a new dashboard in other folder", func() {
|
||||
Convey("When creating a new dashboard in other folder", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: testOrgId,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
@@ -161,7 +165,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
|
||||
err := callSaveWithError(cmd)
|
||||
|
||||
Convey("It should call dashboard guardian with correct arguments and rsult in access denied error", func() {
|
||||
Convey("It should create dashboard guardian for other folder with correct arguments and rsult in access denied error", func() {
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||
|
||||
@@ -171,7 +175,54 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When trying to update a dashboard by existing id in the General folder", func() {
|
||||
Convey("When creating a new dashboard by existing title in folder", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: testOrgId,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"title": savedDashInFolder.Title,
|
||||
}),
|
||||
FolderId: savedFolder.Id,
|
||||
UserId: 10000,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := callSaveWithError(cmd)
|
||||
|
||||
Convey("It should create dashboard guardian for folder with correct arguments and result in access denied error", func() {
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||
|
||||
So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedFolder.Id)
|
||||
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
|
||||
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When creating a new dashboard by existing uid in folder", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: testOrgId,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"uid": savedDashInFolder.Uid,
|
||||
"title": "New dash",
|
||||
}),
|
||||
FolderId: savedFolder.Id,
|
||||
UserId: 10000,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := callSaveWithError(cmd)
|
||||
|
||||
Convey("It should create dashboard guardian for folder with correct arguments and result in access denied error", func() {
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||
|
||||
So(sc.dashboardGuardianMock.DashId, ShouldEqual, savedFolder.Id)
|
||||
So(sc.dashboardGuardianMock.OrgId, ShouldEqual, cmd.OrgId)
|
||||
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When updating a dashboard by existing id in the General folder", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: testOrgId,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
@@ -185,7 +236,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
|
||||
err := callSaveWithError(cmd)
|
||||
|
||||
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
|
||||
Convey("It should create dashboard guardian for dashboard with correct arguments and result in access denied error", func() {
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||
|
||||
@@ -195,7 +246,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When trying to update a dashboard by existing id in other folder", func() {
|
||||
Convey("When updating a dashboard by existing id in other folder", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: testOrgId,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
@@ -209,7 +260,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
|
||||
err := callSaveWithError(cmd)
|
||||
|
||||
Convey("It should call dashboard guardian with correct arguments and result in access denied error", func() {
|
||||
Convey("It should create dashboard guardian for dashboard with correct arguments and result in access denied error", func() {
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldEqual, models.ErrDashboardUpdateAccessDenied)
|
||||
|
||||
@@ -218,6 +269,102 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
So(sc.dashboardGuardianMock.User.UserId, ShouldEqual, cmd.UserId)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When moving a dashboard by existing id to other folder from General folder", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: testOrgId,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": savedDashInGeneralFolder.Id,
|
||||
"title": "Dash",
|
||||
}),
|
||||
FolderId: otherSavedFolder.Id,
|
||||
UserId: 10000,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := callSaveWithError(cmd)
|
||||
|
||||
Convey("It should create dashboard guardian for other folder with correct arguments and result 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 moving a dashboard by existing id to the General folder from other folder", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: testOrgId,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": savedDashInFolder.Id,
|
||||
"title": "Dash",
|
||||
}),
|
||||
FolderId: 0,
|
||||
UserId: 10000,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := callSaveWithError(cmd)
|
||||
|
||||
Convey("It should create dashboard guardian for General folder 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 moving a dashboard by existing uid to other folder from General folder", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: testOrgId,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"uid": savedDashInGeneralFolder.Uid,
|
||||
"title": "Dash",
|
||||
}),
|
||||
FolderId: otherSavedFolder.Id,
|
||||
UserId: 10000,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := callSaveWithError(cmd)
|
||||
|
||||
Convey("It should create dashboard guardian for other folder with correct arguments and result 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 moving a dashboard by existing uid to the General folder from other folder", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: testOrgId,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"uid": savedDashInFolder.Uid,
|
||||
"title": "Dash",
|
||||
}),
|
||||
FolderId: 0,
|
||||
UserId: 10000,
|
||||
Overwrite: true,
|
||||
}
|
||||
|
||||
err := callSaveWithError(cmd)
|
||||
|
||||
Convey("It should create dashboard guardian for General folder 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Given user has permission to save
|
||||
@@ -668,7 +815,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When trying to update existing folder to a dashboard using id", func() {
|
||||
Convey("When updating existing folder to a dashboard using id", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
@@ -687,7 +834,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When trying to update existing dashboard to a folder using id", func() {
|
||||
Convey("When updating existing dashboard to a folder using id", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
@@ -706,7 +853,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When trying to update existing folder to a dashboard using uid", func() {
|
||||
Convey("When updating existing folder to a dashboard using uid", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
@@ -725,7 +872,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When trying to update existing dashboard to a folder using uid", func() {
|
||||
Convey("When updating existing dashboard to a folder using uid", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
@@ -744,7 +891,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When trying to update existing folder to a dashboard using title", func() {
|
||||
Convey("When updating existing folder to a dashboard using title", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
@@ -762,7 +909,7 @@ func TestIntegratedDashboardService(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When trying to update existing dashboard to a folder using title", func() {
|
||||
Convey("When updating existing dashboard to a folder using title", func() {
|
||||
cmd := models.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
@@ -850,23 +997,6 @@ func callSaveWithError(cmd models.SaveDashboardCommand) error {
|
||||
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,
|
||||
|
||||
@@ -80,7 +80,7 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
} else if !has {
|
||||
return m.ErrDashboardSnapshotNotFound
|
||||
}
|
||||
|
||||
|
||||
@@ -104,9 +104,8 @@ func TestDashboardDataAccess(t *testing.T) {
|
||||
timesCalled += 1
|
||||
if timesCalled <= 2 {
|
||||
return savedDash.Uid
|
||||
} else {
|
||||
return util.GenerateShortUid()
|
||||
}
|
||||
return util.GenerateShortUid()
|
||||
}
|
||||
cmd := m.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
|
||||
@@ -90,4 +90,29 @@ func addAnnotationMig(mg *Migrator) {
|
||||
Sqlite(updateTextFieldSql).
|
||||
Postgres(updateTextFieldSql).
|
||||
Mysql(updateTextFieldSql))
|
||||
|
||||
//
|
||||
// Add a 'created' & 'updated' column
|
||||
//
|
||||
mg.AddMigration("Add created time to annotation table", NewAddColumnMigration(table, &Column{
|
||||
Name: "created", Type: DB_BigInt, Nullable: true, Default: "0",
|
||||
}))
|
||||
mg.AddMigration("Add updated time to annotation table", NewAddColumnMigration(table, &Column{
|
||||
Name: "updated", Type: DB_BigInt, Nullable: true, Default: "0",
|
||||
}))
|
||||
mg.AddMigration("Add index for created in annotation table", NewAddIndexMigration(table, &Index{
|
||||
Cols: []string{"org_id", "created"}, Type: IndexType,
|
||||
}))
|
||||
mg.AddMigration("Add index for updated in annotation table", NewAddIndexMigration(table, &Index{
|
||||
Cols: []string{"org_id", "updated"}, Type: IndexType,
|
||||
}))
|
||||
|
||||
//
|
||||
// Convert epoch saved as seconds to miliseconds
|
||||
//
|
||||
updateEpochSql := "UPDATE annotation SET epoch = (epoch*1000) where epoch < 9999999999"
|
||||
mg.AddMigration("Convert existing annotations from seconds to milliseconds", new(RawSqlMigration).
|
||||
Sqlite(updateEpochSql).
|
||||
Postgres(updateEpochSql).
|
||||
Mysql(updateEpochSql))
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ func AddMigrations(mg *Migrator) {
|
||||
addDashboardAclMigrations(mg)
|
||||
addTagMigration(mg)
|
||||
addLoginAttemptMigrations(mg)
|
||||
addUserAuthMigrations(mg)
|
||||
}
|
||||
|
||||
func addMigrationLogMigrations(mg *Migrator) {
|
||||
|
||||
@@ -8,11 +8,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
//"github.com/grafana/grafana/pkg/log"
|
||||
)
|
||||
|
||||
var indexTypes = []string{"Unknown", "INDEX", "UNIQUE INDEX"}
|
||||
|
||||
func TestMigrations(t *testing.T) {
|
||||
testDBs := []sqlutil.TestDB{
|
||||
sqlutil.TestDB_Sqlite3,
|
||||
@@ -30,7 +27,7 @@ func TestMigrations(t *testing.T) {
|
||||
|
||||
sqlutil.CleanDB(x)
|
||||
|
||||
has, err := x.SQL(sql).Get(&r)
|
||||
_, err = x.SQL(sql).Get(&r)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
mg := NewMigrator(x)
|
||||
@@ -39,7 +36,7 @@ func TestMigrations(t *testing.T) {
|
||||
err = mg.Start()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
has, err = x.SQL(sql).Get(&r)
|
||||
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
|
||||
|
||||
@@ -2,37 +2,38 @@ package migrations
|
||||
|
||||
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
|
||||
func addStatsMigrations(mg *Migrator) {
|
||||
statTable := Table{
|
||||
Name: "stat",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_Int, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "metric", Type: DB_Varchar, Length: 20, Nullable: false},
|
||||
{Name: "type", Type: DB_Int, Nullable: false},
|
||||
},
|
||||
Indices: []*Index{
|
||||
{Cols: []string{"metric"}, Type: UniqueIndex},
|
||||
},
|
||||
}
|
||||
|
||||
// create table
|
||||
mg.AddMigration("create stat table", NewAddTableMigration(statTable))
|
||||
|
||||
// create indices
|
||||
mg.AddMigration("add index stat.metric", NewAddIndexMigration(statTable, statTable.Indices[0]))
|
||||
|
||||
statValue := Table{
|
||||
Name: "stat_value",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_Int, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "value", Type: DB_Double, Nullable: false},
|
||||
{Name: "time", Type: DB_DateTime, Nullable: false},
|
||||
},
|
||||
}
|
||||
|
||||
// create table
|
||||
mg.AddMigration("create stat_value table", NewAddTableMigration(statValue))
|
||||
}
|
||||
// commented out because of the deadcode CI check
|
||||
//func addStatsMigrations(mg *Migrator) {
|
||||
// statTable := Table{
|
||||
// Name: "stat",
|
||||
// Columns: []*Column{
|
||||
// {Name: "id", Type: DB_Int, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
// {Name: "metric", Type: DB_Varchar, Length: 20, Nullable: false},
|
||||
// {Name: "type", Type: DB_Int, Nullable: false},
|
||||
// },
|
||||
// Indices: []*Index{
|
||||
// {Cols: []string{"metric"}, Type: UniqueIndex},
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // create table
|
||||
// mg.AddMigration("create stat table", NewAddTableMigration(statTable))
|
||||
//
|
||||
// // create indices
|
||||
// mg.AddMigration("add index stat.metric", NewAddIndexMigration(statTable, statTable.Indices[0]))
|
||||
//
|
||||
// statValue := Table{
|
||||
// Name: "stat_value",
|
||||
// Columns: []*Column{
|
||||
// {Name: "id", Type: DB_Int, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
// {Name: "value", Type: DB_Double, Nullable: false},
|
||||
// {Name: "time", Type: DB_DateTime, Nullable: false},
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // create table
|
||||
// mg.AddMigration("create stat_value table", NewAddTableMigration(statValue))
|
||||
//}
|
||||
|
||||
func addTestDataMigrations(mg *Migrator) {
|
||||
testData := Table{
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package migrations
|
||||
|
||||
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
|
||||
func addUserAuthMigrations(mg *Migrator) {
|
||||
userAuthV1 := Table{
|
||||
Name: "user_auth",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "user_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "auth_module", Type: DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "auth_id", Type: DB_NVarchar, Length: 100, Nullable: false},
|
||||
{Name: "created", Type: DB_DateTime, Nullable: false},
|
||||
},
|
||||
Indices: []*Index{
|
||||
{Cols: []string{"auth_module", "auth_id"}},
|
||||
},
|
||||
}
|
||||
|
||||
// create table
|
||||
mg.AddMigration("create user auth table", NewAddTableMigration(userAuthV1))
|
||||
// add indices
|
||||
addTableIndicesMigrations(mg, "v1", userAuthV1)
|
||||
|
||||
mg.AddMigration("alter user_auth.auth_id to length 190", new(RawSqlMigration).
|
||||
Sqlite("SELECT 0 WHERE 0;").
|
||||
Postgres("ALTER TABLE user_auth ALTER COLUMN auth_id TYPE VARCHAR(190);").
|
||||
Mysql("ALTER TABLE user_auth MODIFY auth_id VARCHAR(190);"))
|
||||
}
|
||||
@@ -84,8 +84,7 @@ func (db *BaseDialect) DateTimeFunc(value string) string {
|
||||
}
|
||||
|
||||
func (b *BaseDialect) CreateTableSql(table *Table) string {
|
||||
var sql string
|
||||
sql = "CREATE TABLE IF NOT EXISTS "
|
||||
sql := "CREATE TABLE IF NOT EXISTS "
|
||||
sql += b.dialect.Quote(table.Name) + " (\n"
|
||||
|
||||
pkList := table.PrimaryKeys
|
||||
@@ -162,8 +161,7 @@ func (db *BaseDialect) RenameTable(oldName string, newName string) string {
|
||||
|
||||
func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
|
||||
quote := db.dialect.Quote
|
||||
var name string
|
||||
name = index.XName(tableName)
|
||||
name := index.XName(tableName)
|
||||
return fmt.Sprintf("DROP INDEX %v ON %s", quote(name), quote(tableName))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -113,7 +112,7 @@ func NewDropIndexMigration(table Table, index *Index) *DropIndexMigration {
|
||||
|
||||
func (m *DropIndexMigration) Sql(dialect Dialect) string {
|
||||
if m.index.Name == "" {
|
||||
m.index.Name = fmt.Sprintf("%s", strings.Join(m.index.Cols, "_"))
|
||||
m.index.Name = strings.Join(m.index.Cols, "_")
|
||||
}
|
||||
return dialect.DropIndexSql(m.tableName, m.index)
|
||||
}
|
||||
@@ -180,7 +179,7 @@ type CopyTableDataMigration struct {
|
||||
targetTable string
|
||||
sourceCols []string
|
||||
targetCols []string
|
||||
colMap map[string]string
|
||||
//colMap map[string]string
|
||||
}
|
||||
|
||||
func NewCopyTableDataMigration(targetTable string, sourceTable string, colMap map[string]string) *CopyTableDataMigration {
|
||||
|
||||
@@ -97,17 +97,15 @@ func (mg *Migrator) Start() error {
|
||||
mg.Logger.Debug("Executing", "sql", sql)
|
||||
|
||||
err := mg.inTransaction(func(sess *xorm.Session) error {
|
||||
|
||||
if err := mg.exec(m, sess); err != nil {
|
||||
err := mg.exec(m, sess)
|
||||
if err != nil {
|
||||
mg.Logger.Error("Exec failed", "error", err, "sql", sql)
|
||||
record.Error = err.Error()
|
||||
sess.Insert(&record)
|
||||
return err
|
||||
} else {
|
||||
record.Success = true
|
||||
sess.Insert(&record)
|
||||
}
|
||||
|
||||
record.Success = true
|
||||
sess.Insert(&record)
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
@@ -66,8 +66,8 @@ func (db *Mysql) SqlType(c *Column) string {
|
||||
res = c.Type
|
||||
}
|
||||
|
||||
var hasLen1 bool = (c.Length > 0)
|
||||
var hasLen2 bool = (c.Length2 > 0)
|
||||
var hasLen1 = (c.Length > 0)
|
||||
var hasLen2 = (c.Length2 > 0)
|
||||
|
||||
if res == DB_BigInt && !hasLen1 && !hasLen2 {
|
||||
c.Length = 20
|
||||
|
||||
@@ -45,9 +45,8 @@ func (b *Postgres) Default(col *Column) string {
|
||||
if col.Type == DB_Bool {
|
||||
if col.Default == "0" {
|
||||
return "FALSE"
|
||||
} else {
|
||||
return "TRUE"
|
||||
}
|
||||
return "TRUE"
|
||||
}
|
||||
return col.Default
|
||||
}
|
||||
@@ -92,8 +91,8 @@ func (db *Postgres) SqlType(c *Column) string {
|
||||
res = t
|
||||
}
|
||||
|
||||
var hasLen1 bool = (c.Length > 0)
|
||||
var hasLen2 bool = (c.Length2 > 0)
|
||||
var hasLen1 = (c.Length > 0)
|
||||
var hasLen2 = (c.Length2 > 0)
|
||||
if hasLen2 {
|
||||
res += "(" + strconv.Itoa(c.Length) + "," + strconv.Itoa(c.Length2) + ")"
|
||||
} else if hasLen1 {
|
||||
|
||||
@@ -46,7 +46,7 @@ type Index struct {
|
||||
|
||||
func (index *Index) XName(tableName string) string {
|
||||
if index.Name == "" {
|
||||
index.Name = fmt.Sprintf("%s", strings.Join(index.Cols, "_"))
|
||||
index.Name = strings.Join(index.Cols, "_")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(index.Name, "UQE_") &&
|
||||
|
||||
@@ -22,6 +22,9 @@ func CreatePlaylist(cmd *m.CreatePlaylistCommand) error {
|
||||
}
|
||||
|
||||
_, err := x.Insert(&playlist)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
playlistItems := make([]m.PlaylistItem, 0)
|
||||
for _, item := range cmd.Items {
|
||||
|
||||
@@ -36,7 +36,7 @@ func GetPluginSettingById(query *m.GetPluginSettingByIdQuery) error {
|
||||
has, err := x.Get(&pluginSetting)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
} else if !has {
|
||||
return m.ErrPluginSettingNotFound
|
||||
}
|
||||
query.Result = &pluginSetting
|
||||
@@ -48,6 +48,9 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error {
|
||||
var pluginSetting m.PluginSetting
|
||||
|
||||
exists, err := sess.Where("org_id=? and plugin_id=?", cmd.OrgId, cmd.PluginId).Get(&pluginSetting)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sess.UseBool("enabled")
|
||||
sess.UseBool("pinned")
|
||||
if !exists {
|
||||
@@ -72,34 +75,33 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error {
|
||||
|
||||
_, err = sess.Insert(&pluginSetting)
|
||||
return err
|
||||
} else {
|
||||
for key, data := range cmd.SecureJsonData {
|
||||
encryptedData, err := util.Encrypt([]byte(data), setting.SecretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pluginSetting.SecureJsonData[key] = encryptedData
|
||||
}
|
||||
|
||||
// add state change event on commit success
|
||||
if pluginSetting.Enabled != cmd.Enabled {
|
||||
sess.events = append(sess.events, &m.PluginStateChangedEvent{
|
||||
PluginId: cmd.PluginId,
|
||||
OrgId: cmd.OrgId,
|
||||
Enabled: cmd.Enabled,
|
||||
})
|
||||
}
|
||||
|
||||
pluginSetting.Updated = time.Now()
|
||||
pluginSetting.Enabled = cmd.Enabled
|
||||
pluginSetting.JsonData = cmd.JsonData
|
||||
pluginSetting.Pinned = cmd.Pinned
|
||||
pluginSetting.PluginVersion = cmd.PluginVersion
|
||||
|
||||
_, err = sess.Id(pluginSetting.Id).Update(&pluginSetting)
|
||||
return err
|
||||
}
|
||||
for key, data := range cmd.SecureJsonData {
|
||||
encryptedData, err := util.Encrypt([]byte(data), setting.SecretKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pluginSetting.SecureJsonData[key] = encryptedData
|
||||
}
|
||||
|
||||
// add state change event on commit success
|
||||
if pluginSetting.Enabled != cmd.Enabled {
|
||||
sess.events = append(sess.events, &m.PluginStateChangedEvent{
|
||||
PluginId: cmd.PluginId,
|
||||
OrgId: cmd.OrgId,
|
||||
Enabled: cmd.Enabled,
|
||||
})
|
||||
}
|
||||
|
||||
pluginSetting.Updated = time.Now()
|
||||
pluginSetting.Enabled = cmd.Enabled
|
||||
pluginSetting.JsonData = cmd.JsonData
|
||||
pluginSetting.Pinned = cmd.Pinned
|
||||
pluginSetting.PluginVersion = cmd.PluginVersion
|
||||
|
||||
_, err = sess.Id(pluginSetting.Id).Update(&pluginSetting)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,9 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error {
|
||||
|
||||
var prefs m.Preferences
|
||||
exists, err := sess.Where("org_id=? AND user_id=?", cmd.OrgId, cmd.UserId).Get(&prefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
prefs = m.Preferences{
|
||||
@@ -85,14 +88,13 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error {
|
||||
}
|
||||
_, err = sess.Insert(&prefs)
|
||||
return err
|
||||
} else {
|
||||
prefs.HomeDashboardId = cmd.HomeDashboardId
|
||||
prefs.Timezone = cmd.Timezone
|
||||
prefs.Theme = cmd.Theme
|
||||
prefs.Updated = time.Now()
|
||||
prefs.Version += 1
|
||||
_, err := sess.Id(prefs.Id).AllCols().Update(&prefs)
|
||||
return err
|
||||
}
|
||||
prefs.HomeDashboardId = cmd.HomeDashboardId
|
||||
prefs.Timezone = cmd.Timezone
|
||||
prefs.Theme = cmd.Theme
|
||||
prefs.Updated = time.Now()
|
||||
prefs.Version += 1
|
||||
_, err = sess.Id(prefs.Id).AllCols().Update(&prefs)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func GetOrgQuotaByTarget(query *m.GetOrgQuotaByTargetQuery) error {
|
||||
has, err := x.Get("a)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
} else if !has {
|
||||
quota.Limit = query.Default
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
|
||||
return err
|
||||
}
|
||||
quota.Limit = cmd.Limit
|
||||
if has == false {
|
||||
if !has {
|
||||
quota.Created = time.Now()
|
||||
//No quota in the DB for this target, so create a new one.
|
||||
if _, err := sess.Insert("a); err != nil {
|
||||
@@ -133,7 +133,7 @@ func GetUserQuotaByTarget(query *m.GetUserQuotaByTargetQuery) error {
|
||||
has, err := x.Get("a)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
} else if !has {
|
||||
quota.Limit = query.Default
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
|
||||
return err
|
||||
}
|
||||
quota.Limit = cmd.Limit
|
||||
if has == false {
|
||||
if !has {
|
||||
quota.Created = time.Now()
|
||||
//No quota in the DB for this target, so create a new one.
|
||||
if _, err := sess.Insert("a); err != nil {
|
||||
|
||||
@@ -77,7 +77,7 @@ func EnsureAdminUser() {
|
||||
log.Info("Created default admin user: %v", setting.AdminUser)
|
||||
}
|
||||
|
||||
func NewEngine() {
|
||||
func NewEngine() *xorm.Engine {
|
||||
x, err := getEngine()
|
||||
|
||||
if err != nil {
|
||||
@@ -91,6 +91,8 @@ func NewEngine() {
|
||||
sqlog.Error("Fail to initialize orm engine", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
func SetEngine(engine *xorm.Engine) (err error) {
|
||||
@@ -121,7 +123,7 @@ func getEngine() (*xorm.Engine, error) {
|
||||
}
|
||||
|
||||
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&allowNativePasswords=true",
|
||||
DbCfg.User, DbCfg.Pwd, protocol, DbCfg.Host, DbCfg.Name)
|
||||
url.QueryEscape(DbCfg.User), url.QueryEscape(DbCfg.Pwd), protocol, DbCfg.Host, url.PathEscape(DbCfg.Name))
|
||||
|
||||
if DbCfg.SslMode == "true" || DbCfg.SslMode == "skip-verify" {
|
||||
tlsCert, err := makeCert("custom", DbCfg)
|
||||
@@ -140,13 +142,17 @@ func getEngine() (*xorm.Engine, error) {
|
||||
if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 {
|
||||
port = fields[1]
|
||||
}
|
||||
if DbCfg.Pwd == "" {
|
||||
DbCfg.Pwd = "''"
|
||||
}
|
||||
if DbCfg.User == "" {
|
||||
DbCfg.User = "''"
|
||||
}
|
||||
cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode, DbCfg.ClientCertPath, DbCfg.ClientKeyPath, DbCfg.CaCertPath)
|
||||
cnnstr = fmt.Sprintf("user='%s' password='%s' host='%s' port='%s' dbname='%s' sslmode='%s' sslcert='%s' sslkey='%s' sslrootcert='%s'",
|
||||
strings.Replace(DbCfg.User, `'`, `\'`, -1),
|
||||
strings.Replace(DbCfg.Pwd, `'`, `\'`, -1),
|
||||
strings.Replace(host, `'`, `\'`, -1),
|
||||
strings.Replace(port, `'`, `\'`, -1),
|
||||
strings.Replace(DbCfg.Name, `'`, `\'`, -1),
|
||||
strings.Replace(DbCfg.SslMode, `'`, `\'`, -1),
|
||||
strings.Replace(DbCfg.ClientCertPath, `'`, `\'`, -1),
|
||||
strings.Replace(DbCfg.ClientKeyPath, `'`, `\'`, -1),
|
||||
strings.Replace(DbCfg.CaCertPath, `'`, `\'`, -1),
|
||||
)
|
||||
case "sqlite3":
|
||||
if !filepath.IsAbs(DbCfg.Path) {
|
||||
DbCfg.Path = filepath.Join(setting.DataPath, DbCfg.Path)
|
||||
@@ -166,7 +172,7 @@ func getEngine() (*xorm.Engine, error) {
|
||||
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)
|
||||
debugSql := setting.Raw.Section("database").Key("log_queries").MustBool(false)
|
||||
if !debugSql {
|
||||
engine.SetLogger(&xorm.DiscardLogger{})
|
||||
} else {
|
||||
@@ -179,7 +185,7 @@ func getEngine() (*xorm.Engine, error) {
|
||||
}
|
||||
|
||||
func LoadConfig() {
|
||||
sec := setting.Cfg.Section("database")
|
||||
sec := setting.Raw.Section("database")
|
||||
|
||||
cfgURL := sec.Key("url").String()
|
||||
if len(cfgURL) != 0 {
|
||||
@@ -258,7 +264,7 @@ func InitTestDB(t *testing.T) *xorm.Engine {
|
||||
// x.ShowSQL()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to init in memory sqllite3 db %v", err)
|
||||
t.Fatalf("Failed to init test database: %v", err)
|
||||
}
|
||||
|
||||
sqlutil.CleanDB(x)
|
||||
@@ -269,3 +275,19 @@ func InitTestDB(t *testing.T) *xorm.Engine {
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
func IsTestDbMySql() bool {
|
||||
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
|
||||
return db == dbMySql
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func IsTestDbPostgres() bool {
|
||||
if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
|
||||
return db == dbPostgres
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -13,16 +13,12 @@ func init() {
|
||||
bus.AddHandler("sql", GetAdminStats)
|
||||
}
|
||||
|
||||
var activeUserTimeLimit time.Duration = time.Hour * 24 * 30
|
||||
var activeUserTimeLimit = time.Hour * 24 * 30
|
||||
|
||||
func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
|
||||
var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
|
||||
query.Result = make([]*m.DataSourceStats, 0)
|
||||
err := x.SQL(rawSql).Find(&query.Result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -68,6 +64,7 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
|
||||
}
|
||||
|
||||
query.Result = &stats
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -210,11 +210,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error {
|
||||
sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId)
|
||||
|
||||
err := sess.Find(&query.Result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// AddTeamMember adds a user to a team
|
||||
|
||||
@@ -74,6 +74,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
Convey("Should be able to return all teams a user is member of", func() {
|
||||
groupId := group2.Result.Id
|
||||
err := AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[0]})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := &m.GetTeamsByUserQuery{OrgId: testOrgId, UserId: userIds[0]}
|
||||
err = GetTeamsByUser(query)
|
||||
@@ -103,7 +104,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: groupId, UserId: userIds[2]})
|
||||
So(err, ShouldBeNil)
|
||||
err = testHelperUpdateDashboardAcl(1, m.DashboardAcl{DashboardId: 1, OrgId: testOrgId, Permission: m.PERMISSION_EDIT, TeamId: groupId})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
err = DeleteTeam(&m.DeleteTeamCommand{OrgId: testOrgId, Id: groupId})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ func GetTempUserByCode(query *m.GetTempUserByCodeQuery) error {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
} else if !has {
|
||||
return m.ErrTempUserNotFound
|
||||
}
|
||||
|
||||
|
||||
@@ -47,10 +47,9 @@ func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *DBSession) (int64, error
|
||||
}
|
||||
if has {
|
||||
return org.Id, nil
|
||||
} else {
|
||||
org.Name = "Main Org."
|
||||
org.Id = 1
|
||||
}
|
||||
org.Name = "Main Org."
|
||||
org.Id = 1
|
||||
} else {
|
||||
org.Name = cmd.OrgName
|
||||
if len(org.Name) == 0 {
|
||||
@@ -154,7 +153,7 @@ func GetUserById(query *m.GetUserByIdQuery) error {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
} else if !has {
|
||||
return m.ErrUserNotFound
|
||||
}
|
||||
|
||||
@@ -168,18 +167,16 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
|
||||
return m.ErrUserNotFound
|
||||
}
|
||||
|
||||
user := new(m.User)
|
||||
|
||||
// Try and find the user by login first.
|
||||
// It's not sufficient to assume that a LoginOrEmail with an "@" is an email.
|
||||
user = &m.User{Login: query.LoginOrEmail}
|
||||
user := &m.User{Login: query.LoginOrEmail}
|
||||
has, err := x.Get(user)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if has == false && strings.Contains(query.LoginOrEmail, "@") {
|
||||
if !has && strings.Contains(query.LoginOrEmail, "@") {
|
||||
// If the user wasn't found, and it contains an "@" fallback to finding the
|
||||
// user by email.
|
||||
user = &m.User{Email: query.LoginOrEmail}
|
||||
@@ -188,7 +185,7 @@ func GetUserByLogin(query *m.GetUserByLoginQuery) error {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
} else if !has {
|
||||
return m.ErrUserNotFound
|
||||
}
|
||||
|
||||
@@ -202,14 +199,12 @@ func GetUserByEmail(query *m.GetUserByEmailQuery) error {
|
||||
return m.ErrUserNotFound
|
||||
}
|
||||
|
||||
user := new(m.User)
|
||||
|
||||
user = &m.User{Email: query.Email}
|
||||
user := &m.User{Email: query.Email}
|
||||
has, err := x.Get(user)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
} else if !has {
|
||||
return m.ErrUserNotFound
|
||||
}
|
||||
|
||||
@@ -253,11 +248,8 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error {
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err := sess.Id(cmd.UserId).Update(&user)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -271,11 +263,8 @@ func UpdateUserLastSeenAt(cmd *m.UpdateUserLastSeenAtCommand) error {
|
||||
LastSeenAt: time.Now(),
|
||||
}
|
||||
|
||||
if _, err := sess.Id(cmd.UserId).Update(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err := sess.Id(cmd.UserId).Update(&user)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -295,11 +284,12 @@ func SetUsingOrg(cmd *m.SetUsingOrgCommand) error {
|
||||
}
|
||||
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
user := m.User{}
|
||||
sess.Id(cmd.UserId).Get(&user)
|
||||
user := m.User{
|
||||
Id: cmd.UserId,
|
||||
OrgId: cmd.OrgId,
|
||||
}
|
||||
|
||||
user.OrgId = cmd.OrgId
|
||||
_, err := sess.Id(user.Id).Update(&user)
|
||||
_, err := sess.Id(cmd.UserId).Update(&user)
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -310,7 +300,7 @@ func GetUserProfile(query *m.GetUserProfileQuery) error {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has == false {
|
||||
} else if !has {
|
||||
return m.ErrUserNotFound
|
||||
}
|
||||
|
||||
@@ -333,6 +323,7 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error {
|
||||
sess.Join("INNER", "org", "org_user.org_id=org.id")
|
||||
sess.Where("org_user.user_id=?", query.UserId)
|
||||
sess.Cols("org.name", "org_user.role", "org_user.org_id")
|
||||
sess.OrderBy("org.name")
|
||||
err := sess.Find(&query.Result)
|
||||
return err
|
||||
}
|
||||
@@ -444,6 +435,7 @@ func DeleteUser(cmd *m.DeleteUserCommand) error {
|
||||
"DELETE FROM dashboard_acl WHERE user_id = ?",
|
||||
"DELETE FROM preferences WHERE user_id = ?",
|
||||
"DELETE FROM team_member WHERE user_id = ?",
|
||||
"DELETE FROM user_auth WHERE user_id = ?",
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
@@ -478,10 +470,7 @@ func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error {
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
if _, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
_, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("sql", GetUserByAuthInfo)
|
||||
bus.AddHandler("sql", GetAuthInfo)
|
||||
bus.AddHandler("sql", SetAuthInfo)
|
||||
bus.AddHandler("sql", DeleteAuthInfo)
|
||||
}
|
||||
|
||||
func GetUserByAuthInfo(query *m.GetUserByAuthInfoQuery) error {
|
||||
user := &m.User{}
|
||||
has := false
|
||||
var err error
|
||||
authQuery := &m.GetAuthInfoQuery{}
|
||||
|
||||
// Try to find the user by auth module and id first
|
||||
if query.AuthModule != "" && query.AuthId != "" {
|
||||
authQuery.AuthModule = query.AuthModule
|
||||
authQuery.AuthId = query.AuthId
|
||||
|
||||
err = GetAuthInfo(authQuery)
|
||||
if err != m.ErrUserNotFound {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if user id was specified and doesn't match the user_auth entry, remove it
|
||||
if query.UserId != 0 && query.UserId != authQuery.Result.UserId {
|
||||
err = DeleteAuthInfo(&m.DeleteAuthInfoCommand{
|
||||
UserAuth: authQuery.Result,
|
||||
})
|
||||
if err != nil {
|
||||
sqlog.Error("Error removing user_auth entry", "error", err)
|
||||
}
|
||||
|
||||
authQuery.Result = nil
|
||||
} else {
|
||||
has, err = x.Id(authQuery.Result.UserId).Get(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !has {
|
||||
// if the user has been deleted then remove the entry
|
||||
err = DeleteAuthInfo(&m.DeleteAuthInfoCommand{
|
||||
UserAuth: authQuery.Result,
|
||||
})
|
||||
if err != nil {
|
||||
sqlog.Error("Error removing user_auth entry", "error", err)
|
||||
}
|
||||
|
||||
authQuery.Result = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try to find the user by id
|
||||
if !has && query.UserId != 0 {
|
||||
has, err = x.Id(query.UserId).Get(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try to find the user by email address
|
||||
if !has && query.Email != "" {
|
||||
user = &m.User{Email: query.Email}
|
||||
has, err = x.Get(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try to find the user by login
|
||||
if !has && query.Login != "" {
|
||||
user = &m.User{Login: query.Login}
|
||||
has, err = x.Get(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// No user found
|
||||
if !has {
|
||||
return m.ErrUserNotFound
|
||||
}
|
||||
|
||||
// create authInfo record to link accounts
|
||||
if authQuery.Result == nil && query.AuthModule != "" && query.AuthId != "" {
|
||||
cmd2 := &m.SetAuthInfoCommand{
|
||||
UserId: user.Id,
|
||||
AuthModule: query.AuthModule,
|
||||
AuthId: query.AuthId,
|
||||
}
|
||||
if err := SetAuthInfo(cmd2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
query.Result = user
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAuthInfo(query *m.GetAuthInfoQuery) error {
|
||||
userAuth := &m.UserAuth{
|
||||
AuthModule: query.AuthModule,
|
||||
AuthId: query.AuthId,
|
||||
}
|
||||
has, err := x.Get(userAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return m.ErrUserNotFound
|
||||
}
|
||||
|
||||
query.Result = userAuth
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetAuthInfo(cmd *m.SetAuthInfoCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
authUser := &m.UserAuth{
|
||||
UserId: cmd.UserId,
|
||||
AuthModule: cmd.AuthModule,
|
||||
AuthId: cmd.AuthId,
|
||||
Created: time.Now(),
|
||||
}
|
||||
|
||||
_, err := sess.Insert(authUser)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteAuthInfo(cmd *m.DeleteAuthInfoCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
_, err := sess.Delete(cmd.UserAuth)
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
func TestUserAuth(t *testing.T) {
|
||||
InitTestDB(t)
|
||||
|
||||
Convey("Given 5 users", t, func() {
|
||||
var err error
|
||||
var cmd *m.CreateUserCommand
|
||||
users := []m.User{}
|
||||
for i := 0; i < 5; i++ {
|
||||
cmd = &m.CreateUserCommand{
|
||||
Email: fmt.Sprint("user", i, "@test.com"),
|
||||
Name: fmt.Sprint("user", i),
|
||||
Login: fmt.Sprint("loginuser", i),
|
||||
}
|
||||
err = CreateUser(cmd)
|
||||
So(err, ShouldBeNil)
|
||||
users = append(users, cmd.Result)
|
||||
}
|
||||
|
||||
Reset(func() {
|
||||
_, err := x.Exec("DELETE FROM org_user WHERE 1=1")
|
||||
So(err, ShouldBeNil)
|
||||
_, err = x.Exec("DELETE FROM org WHERE 1=1")
|
||||
So(err, ShouldBeNil)
|
||||
_, err = x.Exec("DELETE FROM " + dialect.Quote("user") + " WHERE 1=1")
|
||||
So(err, ShouldBeNil)
|
||||
_, err = x.Exec("DELETE FROM user_auth WHERE 1=1")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Can find existing user", func() {
|
||||
// By Login
|
||||
login := "loginuser0"
|
||||
|
||||
query := &m.GetUserByAuthInfoQuery{Login: login}
|
||||
err = GetUserByAuthInfo(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.Login, ShouldEqual, login)
|
||||
|
||||
// By ID
|
||||
id := query.Result.Id
|
||||
|
||||
query = &m.GetUserByAuthInfoQuery{UserId: id}
|
||||
err = GetUserByAuthInfo(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.Id, ShouldEqual, id)
|
||||
|
||||
// By Email
|
||||
email := "user1@test.com"
|
||||
|
||||
query = &m.GetUserByAuthInfoQuery{Email: email}
|
||||
err = GetUserByAuthInfo(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.Email, ShouldEqual, email)
|
||||
|
||||
// Don't find nonexistent user
|
||||
email = "nonexistent@test.com"
|
||||
|
||||
query = &m.GetUserByAuthInfoQuery{Email: email}
|
||||
err = GetUserByAuthInfo(query)
|
||||
|
||||
So(err, ShouldEqual, m.ErrUserNotFound)
|
||||
So(query.Result, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Can set & locate by AuthModule and AuthId", func() {
|
||||
// get nonexistent user_auth entry
|
||||
query := &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
|
||||
err = GetUserByAuthInfo(query)
|
||||
|
||||
So(err, ShouldEqual, m.ErrUserNotFound)
|
||||
So(query.Result, ShouldBeNil)
|
||||
|
||||
// create user_auth entry
|
||||
login := "loginuser0"
|
||||
|
||||
query.Login = login
|
||||
err = GetUserByAuthInfo(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.Login, ShouldEqual, login)
|
||||
|
||||
// get via user_auth
|
||||
query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
|
||||
err = GetUserByAuthInfo(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.Login, ShouldEqual, login)
|
||||
|
||||
// get with non-matching id
|
||||
id := query.Result.Id
|
||||
|
||||
query.UserId = id + 1
|
||||
err = GetUserByAuthInfo(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.Login, ShouldEqual, "loginuser1")
|
||||
|
||||
// get via user_auth
|
||||
query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
|
||||
err = GetUserByAuthInfo(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result.Login, ShouldEqual, "loginuser1")
|
||||
|
||||
// remove user
|
||||
_, err = x.Exec("DELETE FROM "+dialect.Quote("user")+" WHERE id=?", query.Result.Id)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// get via user_auth for deleted user
|
||||
query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"}
|
||||
err = GetUserByAuthInfo(query)
|
||||
|
||||
So(err, ShouldEqual, m.ErrUserNotFound)
|
||||
So(query.Result, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user