From 7858965117696b47c660bd835ae41686e3b604b8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 18 Jan 2018 12:04:12 +0100 Subject: [PATCH 001/110] codestyle: extract code into methods --- .../wrapper/datasource_plugin_wrapper.go | 12 ++-- .../provisioning/dashboards/file_reader.go | 59 ++++++++++++------- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/pkg/plugins/datasource/wrapper/datasource_plugin_wrapper.go b/pkg/plugins/datasource/wrapper/datasource_plugin_wrapper.go index 490fa0f573a..0afa2a400e4 100644 --- a/pkg/plugins/datasource/wrapper/datasource_plugin_wrapper.go +++ b/pkg/plugins/datasource/wrapper/datasource_plugin_wrapper.go @@ -28,12 +28,12 @@ func (tw *DatasourcePluginWrapper) Query(ctx context.Context, ds *models.DataSou pbQuery := &datasource.DatasourceRequest{ Datasource: &datasource.DatasourceInfo{ - Name: ds.Name, - Type: ds.Type, - Url: ds.Url, - Id: ds.Id, - OrgId: ds.OrgId, - JsonData: string(jsonData), + Name: ds.Name, + Type: ds.Type, + Url: ds.Url, + Id: ds.Id, + OrgId: ds.OrgId, + JsonData: string(jsonData), DecryptedSecureJsonData: ds.SecureJsonData.Decrypt(), }, TimeRange: &datasource.TimeRange{ diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index fbe1a03e287..6b9eeec9e48 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -135,35 +135,39 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (i return cmd.Result.Id, nil } +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 + } + + return fi, nil + } + + return fileinfo, err +} + func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc { return func(path string, fileInfo os.FileInfo, err error) error { if err != nil { return err } - if fileInfo.IsDir() { - if strings.HasPrefix(fileInfo.Name(), ".") { - return filepath.SkipDir - } - return nil + + isValid, err := validateWalkablePath(fileInfo) + if !isValid { + return err } - if !strings.HasSuffix(fileInfo.Name(), ".json") { - return nil - } - - checkFilepath, err := filepath.EvalSymlinks(path) - - if path != checkFilepath { - path = checkFilepath - fi, err := os.Lstat(checkFilepath) - if err != nil { - return err - } - fileInfo = fi + resolvedFileInfo, err := resolveSymlink(fileInfo, path) + if err != nil { + return err } cachedDashboard, exist := fr.cache.getCache(path) - if exist && cachedDashboard.UpdatedAt == fileInfo.ModTime() { + if exist && cachedDashboard.UpdatedAt == resolvedFileInfo.ModTime() { return nil } @@ -194,7 +198,7 @@ func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc { } // break if db version is newer then fil version - if cmd.Result.Updated.Unix() >= fileInfo.ModTime().Unix() { + if cmd.Result.Updated.Unix() >= resolvedFileInfo.ModTime().Unix() { return nil } @@ -205,6 +209,21 @@ func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc { } } +func validateWalkablePath(fileInfo os.FileInfo) (bool, error) { + if fileInfo.IsDir() { + if strings.HasPrefix(fileInfo.Name(), ".") { + return false, filepath.SkipDir + } + return false, nil + } + + if !strings.HasSuffix(fileInfo.Name(), ".json") { + return false, nil + } + + return true, nil +} + func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashboards.SaveDashboardItem, error) { reader, err := os.Open(path) if err != nil { From 67a9e6a71d9c0cbcaf1343e5ef08dd9021c3a51b Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 23 Jan 2018 12:28:56 +0100 Subject: [PATCH 002/110] provisioing: add lookup table provisioned dashboards --- conf/provisioning/dashboards/sample.yaml | 2 +- pkg/api/dashboard.go | 2 +- pkg/api/dashboard_test.go | 14 +- pkg/models/dashboard_provisioning.go | 2 + pkg/models/dashboards.go | 21 ++ pkg/services/dashboards/dashboards.go | 94 ++++++-- .../dashboards/dashboard_cache.go | 6 +- .../provisioning/dashboards/file_reader.go | 47 ++-- .../dashboards/file_reader_test.go | 14 +- pkg/services/provisioning/dashboards/types.go | 4 +- pkg/services/sqlstore/dashboard.go | 221 +++++++++--------- .../sqlstore/dashboard_provisioning.go | 66 ++++++ .../sqlstore/dashboard_provisioning_test.go | 48 ++++ .../sqlstore/migrations/dashboard_mig.go | 17 ++ pkg/services/sqlstore/playlist.go | 4 - 15 files changed, 400 insertions(+), 162 deletions(-) create mode 100644 pkg/models/dashboard_provisioning.go create mode 100644 pkg/services/sqlstore/dashboard_provisioning.go create mode 100644 pkg/services/sqlstore/dashboard_provisioning_test.go diff --git a/conf/provisioning/dashboards/sample.yaml b/conf/provisioning/dashboards/sample.yaml index 40992d1461e..e40612af508 100644 --- a/conf/provisioning/dashboards/sample.yaml +++ b/conf/provisioning/dashboards/sample.yaml @@ -3,4 +3,4 @@ # folder: '' # type: file # options: -# folder: /var/lib/grafana/dashboards \ No newline at end of file +# folder: /var/lib/grafana/dashboards diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 87c42884e31..28a955d5abf 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -182,7 +182,7 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response { } } - dashItem := &dashboards.SaveDashboardItem{ + dashItem := &dashboards.SaveDashboardDTO{ Dashboard: dash, Message: cmd.Message, OrgId: c.OrgId, diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index e6228878625..ed76544b9b8 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -21,15 +21,25 @@ import ( ) type fakeDashboardRepo struct { - inserted []*dashboards.SaveDashboardItem + inserted []*dashboards.SaveDashboardDTO + provisioned []*m.DashboardProvisioning getDashboard []*m.Dashboard } -func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*m.Dashboard, error) { +func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardDTO) (*m.Dashboard, error) { repo.inserted = append(repo.inserted, json) return json.Dashboard, nil } +func (repo *fakeDashboardRepo) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *m.DashboardProvisioning) (*m.Dashboard, error) { + repo.inserted = append(repo.inserted, dto) + return dto.Dashboard, nil +} + +func (repo *fakeDashboardRepo) GetProvisionedDashboardData(name string) ([]*m.DashboardProvisioning, error) { + return repo.provisioned, nil +} + var fakeRepo *fakeDashboardRepo func TestDashboardApiEndpoint(t *testing.T) { diff --git a/pkg/models/dashboard_provisioning.go b/pkg/models/dashboard_provisioning.go new file mode 100644 index 00000000000..d6a05a41090 --- /dev/null +++ b/pkg/models/dashboard_provisioning.go @@ -0,0 +1,2 @@ +package models + diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 091f27ec413..577e97c657d 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -167,6 +167,21 @@ type SaveDashboardCommand struct { Result *Dashboard } +type DashboardProvisioning struct { + Id int64 + DashboardId int64 + Name string + ExternalId string + Updated time.Time +} + +type SaveProvisionedDashboardCommand struct { + DashboardCmd *SaveDashboardCommand + DashboardProvisioning *DashboardProvisioning + + Result *Dashboard +} + type DeleteDashboardCommand struct { Id int64 OrgId int64 @@ -209,3 +224,9 @@ type GetDashboardSlugByIdQuery struct { Id int64 Result string } + +type GetProvisionedDashboardDataQuery struct { + Name string + + Result []*DashboardProvisioning +} diff --git a/pkg/services/dashboards/dashboards.go b/pkg/services/dashboards/dashboards.go index 4bdba59b18e..ce60fde0454 100644 --- a/pkg/services/dashboards/dashboards.go +++ b/pkg/services/dashboards/dashboards.go @@ -9,7 +9,9 @@ import ( ) type Repository interface { - SaveDashboard(*SaveDashboardItem) (*models.Dashboard, error) + SaveDashboard(*SaveDashboardDTO) (*models.Dashboard, error) + SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) + GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) } var repositoryInstance Repository @@ -22,7 +24,7 @@ func SetRepository(rep Repository) { repositoryInstance = rep } -type SaveDashboardItem struct { +type SaveDashboardDTO struct { OrgId int64 UpdatedAt time.Time UserId int64 @@ -33,15 +35,25 @@ type SaveDashboardItem struct { type DashboardRepository struct{} -func (dr *DashboardRepository) SaveDashboard(json *SaveDashboardItem) (*models.Dashboard, error) { - dashboard := json.Dashboard +func (dr *DashboardRepository) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { + cmd := &models.GetProvisionedDashboardDataQuery{Name: name} + err := bus.Dispatch(cmd) + if err != nil { + return nil, err + } + + return cmd.Result, nil +} + +func (dr *DashboardRepository) buildSaveDashboardCommand(dto *SaveDashboardDTO) (*models.SaveDashboardCommand, error) { + dashboard := dto.Dashboard if dashboard.Title == "" { return nil, models.ErrDashboardTitleEmpty } validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{ - OrgId: json.OrgId, + OrgId: dto.OrgId, Dashboard: dashboard, } @@ -49,33 +61,77 @@ func (dr *DashboardRepository) SaveDashboard(json *SaveDashboardItem) (*models.D return nil, models.ErrDashboardContainsInvalidAlertData } - cmd := models.SaveDashboardCommand{ + cmd := &models.SaveDashboardCommand{ Dashboard: dashboard.Data, - Message: json.Message, - OrgId: json.OrgId, - Overwrite: json.Overwrite, - UserId: json.UserId, + Message: dto.Message, + OrgId: dto.OrgId, + Overwrite: dto.Overwrite, + UserId: dto.UserId, FolderId: dashboard.FolderId, IsFolder: dashboard.IsFolder, } - if !json.UpdatedAt.IsZero() { - cmd.UpdatedAt = json.UpdatedAt + if !dto.UpdatedAt.IsZero() { + cmd.UpdatedAt = dto.UpdatedAt } - err := bus.Dispatch(&cmd) - if err != nil { - return nil, err - } + return cmd, nil +} +func (dr *DashboardRepository) updateAlerting(cmd *models.SaveDashboardCommand, dto *SaveDashboardDTO) error { alertCmd := alerting.UpdateDashboardAlertsCommand{ - OrgId: json.OrgId, - UserId: json.UserId, + OrgId: dto.OrgId, + UserId: dto.UserId, Dashboard: cmd.Result, } if err := bus.Dispatch(&alertCmd); err != nil { - return nil, models.ErrDashboardFailedToUpdateAlertData + return models.ErrDashboardFailedToUpdateAlertData + } + + return nil +} + +func (dr *DashboardRepository) SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { + cmd, err := dr.buildSaveDashboardCommand(dto) + if err != nil { + return nil, err + } + + saveCmd := &models.SaveProvisionedDashboardCommand{ + DashboardCmd: cmd, + DashboardProvisioning: provisioning, + } + + // dashboard + err = bus.Dispatch(saveCmd) + if err != nil { + return nil, err + } + + //alerts + err = dr.updateAlerting(cmd, dto) + if err != nil { + return nil, err + } + + return cmd.Result, nil +} + +func (dr *DashboardRepository) SaveDashboard(dto *SaveDashboardDTO) (*models.Dashboard, error) { + cmd, err := dr.buildSaveDashboardCommand(dto) + if err != nil { + return nil, err + } + + err = bus.Dispatch(&cmd) + if err != nil { + return nil, err + } + + err = dr.updateAlerting(cmd, dto) + if err != nil { + return nil, err } return cmd.Result, nil diff --git a/pkg/services/provisioning/dashboards/dashboard_cache.go b/pkg/services/provisioning/dashboards/dashboard_cache.go index da6b7e8a5e8..7f8f6fe490f 100644 --- a/pkg/services/provisioning/dashboards/dashboard_cache.go +++ b/pkg/services/provisioning/dashboards/dashboard_cache.go @@ -14,17 +14,17 @@ func NewDashboardCache() *dashboardCache { return &dashboardCache{internalCache: gocache.New(5*time.Minute, 30*time.Minute)} } -func (fr *dashboardCache) addDashboardCache(key string, json *dashboards.SaveDashboardItem) { +func (fr *dashboardCache) addDashboardCache(key string, json *dashboards.SaveDashboardDTO) { fr.internalCache.Add(key, json, time.Minute*10) } -func (fr *dashboardCache) getCache(key string) (*dashboards.SaveDashboardItem, bool) { +func (fr *dashboardCache) getCache(key string) (*dashboards.SaveDashboardDTO, bool) { obj, exist := fr.internalCache.Get(key) if !exist { return nil, exist } - dash, ok := obj.(*dashboards.SaveDashboardItem) + dash, ok := obj.(*dashboards.SaveDashboardDTO) if !ok { return nil, ok } diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 6b9eeec9e48..91aa6169390 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -25,12 +25,12 @@ var ( ) type fileReader struct { - Cfg *DashboardsAsConfig - Path string - log log.Logger - dashboardRepo dashboards.Repository - cache *dashboardCache - createWalk func(fr *fileReader, folderId int64) filepath.WalkFunc + Cfg *DashboardsAsConfig + Path string + log log.Logger + dashboardRepo dashboards.Repository + cache *dashboardCache + createWalk func(fr *fileReader, folderId int64) filepath.WalkFunc } func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) { @@ -50,28 +50,28 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade } return &fileReader{ - Cfg: cfg, - Path: path, - log: log, - dashboardRepo: dashboards.GetRepository(), - cache: NewDashboardCache(), - createWalk: createWalkFn, + Cfg: cfg, + Path: path, + log: log, + dashboardRepo: dashboards.GetRepository(), + cache: NewDashboardCache(), + createWalk: createWalkFn, }, nil } func (fr *fileReader) ReadAndListen(ctx context.Context) error { - ticker := time.NewTicker(checkDiskForChangesInterval) - if err := fr.startWalkingDisk(); err != nil { fr.log.Error("failed to search for dashboards", "error", err) } + ticker := time.NewTicker(checkDiskForChangesInterval) + running := false for { select { case <-ticker.C: - if !running { // avoid walking the filesystem in parallel. incase fs is very slow. + if !running { // avoid walking the filesystem in parallel. in-case fs is very slow. running = true go func() { if err := fr.startWalkingDisk(); err != nil { @@ -115,7 +115,7 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (i // dashboard folder not found. create one. if err == models.ErrDashboardNotFound { - dash := &dashboards.SaveDashboardItem{} + dash := &dashboards.SaveDashboardDTO{} dash.Dashboard = models.NewDashboard(cfg.Folder) dash.Dashboard.IsFolder = true dash.Overwrite = true @@ -129,7 +129,7 @@ func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (i } if !cmd.Result.IsFolder { - return 0, fmt.Errorf("Got invalid response. Expected folder, found dashboard") + return 0, fmt.Errorf("got invalid response. expected folder, found dashboard") } return cmd.Result.Id, nil @@ -188,7 +188,7 @@ func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc { // if we don't have the dashboard in the db, save it! if err == models.ErrDashboardNotFound { fr.log.Debug("saving new dashboard", "file", path) - _, err = fr.dashboardRepo.SaveDashboard(dash) + err = saveDashboard(fr, path, dash, fileInfo.ModTime()) return err } @@ -203,11 +203,18 @@ func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc { } fr.log.Debug("loading dashboard from disk into database.", "file", path) - _, err = fr.dashboardRepo.SaveDashboard(dash) + err = saveDashboard(fr, path, dash, fileInfo.ModTime()) return err } } +func saveDashboard(fr *fileReader, path string, dash *dashboards.SaveDashboardDTO, modTime time.Time) error { + //dash.Extras["provisioning.filepath"] = path + _, err := fr.dashboardRepo.SaveDashboard(dash) + + + return err +} func validateWalkablePath(fileInfo os.FileInfo) (bool, error) { if fileInfo.IsDir() { @@ -224,7 +231,7 @@ func validateWalkablePath(fileInfo os.FileInfo) (bool, error) { return true, nil } -func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashboards.SaveDashboardItem, error) { +func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashboards.SaveDashboardDTO, error) { reader, err := os.Open(path) if err != nil { return nil, err diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index f2805196dde..5c90e573f80 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -241,15 +241,25 @@ func (ffi FakeFileInfo) Sys() interface{} { } type fakeDashboardRepo struct { - inserted []*dashboards.SaveDashboardItem + inserted []*dashboards.SaveDashboardDTO + provisioned []*models.DashboardProvisioning getDashboard []*models.Dashboard } -func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*models.Dashboard, error) { +func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardDTO) (*models.Dashboard, error) { repo.inserted = append(repo.inserted, json) return json.Dashboard, nil } +func (repo *fakeDashboardRepo) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { + return repo.provisioned, nil +} + +func (repo *fakeDashboardRepo) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { + repo.inserted = append(repo.inserted, dto) + return dto.Dashboard, nil +} + func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error { for _, d := range fakeRepo.getDashboard { if d.Slug == cmd.Slug { diff --git a/pkg/services/provisioning/dashboards/types.go b/pkg/services/provisioning/dashboards/types.go index cf65c65348c..91379b33148 100644 --- a/pkg/services/provisioning/dashboards/types.go +++ b/pkg/services/provisioning/dashboards/types.go @@ -18,8 +18,8 @@ type DashboardsAsConfig struct { Options map[string]interface{} `json:"options" yaml:"options"` } -func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardItem, error) { - dash := &dashboards.SaveDashboardItem{} +func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) { + dash := &dashboards.SaveDashboardDTO{} dash.Dashboard = models.NewDashboardFromJson(data) dash.UpdatedAt = lastModified dash.Overwrite = true diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 0b6b60a5e11..4c72e3423be 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -22,121 +22,126 @@ func init() { func SaveDashboard(cmd *m.SaveDashboardCommand) error { return inTransaction(func(sess *DBSession) error { - dash := cmd.GetDashboardModel() + return saveDashboard(sess, cmd) + }) +} - // try get existing dashboard - var existing, sameTitle m.Dashboard +func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error { + dash := cmd.GetDashboardModel() - if dash.Id > 0 { - dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing) - if err != nil { + // try get existing dashboard + var existing, sameTitle m.Dashboard + + if dash.Id > 0 { + dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing) + if err != nil { + return err + } + if !dashWithIdExists { + return m.ErrDashboardNotFound + } + + // check for is someone else has written in between + if dash.Version != existing.Version { + if cmd.Overwrite { + dash.Version = existing.Version + } else { + return m.ErrDashboardVersionMismatch + } + } + + // do not allow plugin dashboard updates without overwrite flag + if existing.PluginId != "" && cmd.Overwrite == false { + return m.UpdatePluginDashboardError{PluginId: existing.PluginId} + } + } + + sameTitleExists, err := sess.Where("org_id=? AND slug=?", dash.OrgId, dash.Slug).Get(&sameTitle) + if err != nil { + return err + } + + if sameTitleExists { + // another dashboard with same name + if dash.Id != sameTitle.Id { + if cmd.Overwrite { + dash.Id = sameTitle.Id + dash.Version = sameTitle.Version + } else { + return m.ErrDashboardWithSameNameExists + } + } + } + + err = setHasAcl(sess, dash) + if err != nil { + return err + } + + parentVersion := dash.Version + affectedRows := int64(0) + + if dash.Id == 0 { + dash.Version = 1 + metrics.M_Api_Dashboard_Insert.Inc() + dash.Data.Set("version", dash.Version) + affectedRows, err = sess.Insert(dash) + } else { + dash.Version++ + dash.Data.Set("version", dash.Version) + + if !cmd.UpdatedAt.IsZero() { + dash.Updated = cmd.UpdatedAt + } + + affectedRows, err = sess.MustCols("folder_id", "has_acl").Id(dash.Id).Update(dash) + } + + if err != nil { + return err + } + + if affectedRows == 0 { + return m.ErrDashboardNotFound + } + + dashVersion := &m.DashboardVersion{ + DashboardId: dash.Id, + ParentVersion: parentVersion, + RestoredFrom: cmd.RestoredFrom, + Version: dash.Version, + Created: time.Now(), + CreatedBy: dash.UpdatedBy, + Message: cmd.Message, + Data: dash.Data, + } + + // insert version entry + if affectedRows, err = sess.Insert(dashVersion); err != nil { + return err + } else if affectedRows == 0 { + return m.ErrDashboardNotFound + } + + // delete existing tags + _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id) + if err != nil { + return err + } + + // insert new tags + tags := dash.GetTags() + if len(tags) > 0 { + for _, tag := range tags { + if _, err := sess.Insert(&DashboardTag{DashboardId: dash.Id, Term: tag}); err != nil { return err } - if !dashWithIdExists { - return m.ErrDashboardNotFound - } - - // check for is someone else has written in between - if dash.Version != existing.Version { - if cmd.Overwrite { - dash.Version = existing.Version - } else { - return m.ErrDashboardVersionMismatch - } - } - - // do not allow plugin dashboard updates without overwrite flag - if existing.PluginId != "" && cmd.Overwrite == false { - return m.UpdatePluginDashboardError{PluginId: existing.PluginId} - } } + } - sameTitleExists, err := sess.Where("org_id=? AND slug=?", dash.OrgId, dash.Slug).Get(&sameTitle) - if err != nil { - return err - } + cmd.Result = dash - if sameTitleExists { - // another dashboard with same name - if dash.Id != sameTitle.Id { - if cmd.Overwrite { - dash.Id = sameTitle.Id - dash.Version = sameTitle.Version - } else { - return m.ErrDashboardWithSameNameExists - } - } - } - - err = setHasAcl(sess, dash) - if err != nil { - return err - } - - parentVersion := dash.Version - affectedRows := int64(0) - - if dash.Id == 0 { - dash.Version = 1 - metrics.M_Api_Dashboard_Insert.Inc() - dash.Data.Set("version", dash.Version) - affectedRows, err = sess.Insert(dash) - } else { - dash.Version++ - dash.Data.Set("version", dash.Version) - - if !cmd.UpdatedAt.IsZero() { - dash.Updated = cmd.UpdatedAt - } - - affectedRows, err = sess.MustCols("folder_id", "has_acl").Id(dash.Id).Update(dash) - } - - if err != nil { - return err - } - - if affectedRows == 0 { - return m.ErrDashboardNotFound - } - - dashVersion := &m.DashboardVersion{ - DashboardId: dash.Id, - ParentVersion: parentVersion, - RestoredFrom: cmd.RestoredFrom, - Version: dash.Version, - Created: time.Now(), - CreatedBy: dash.UpdatedBy, - Message: cmd.Message, - Data: dash.Data, - } - - // insert version entry - if affectedRows, err = sess.Insert(dashVersion); err != nil { - return err - } else if affectedRows == 0 { - return m.ErrDashboardNotFound - } - - // delete existing tags - _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.Id) - if err != nil { - return err - } - - // insert new tags - tags := dash.GetTags() - if len(tags) > 0 { - for _, tag := range tags { - if _, err := sess.Insert(&DashboardTag{DashboardId: dash.Id, Term: tag}); err != nil { - return err - } - } - } - cmd.Result = dash - - return err - }) + return err } func setHasAcl(sess *DBSession, dash *m.Dashboard) error { diff --git a/pkg/services/sqlstore/dashboard_provisioning.go b/pkg/services/sqlstore/dashboard_provisioning.go new file mode 100644 index 00000000000..91d63e82a28 --- /dev/null +++ b/pkg/services/sqlstore/dashboard_provisioning.go @@ -0,0 +1,66 @@ +package sqlstore + +import ( + "time" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", GetProvisionedDashboardDataQuery) + bus.AddHandler("sql", SaveProvisionedDashboard) +} + +type DashboardExtras struct { + Id int64 + DashboardId int64 + Key string + Value string +} + +func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error { + return inTransaction(func(sess *DBSession) error { + err := saveDashboard(sess, cmd.DashboardCmd) + + if err != nil { + return err + } + + cmd.Result = cmd.DashboardCmd.Result + return saveProvionedData(sess, cmd.DashboardProvisioning) + }) +} + +func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning) error { + results := &models.DashboardProvisioning{} + + exist, err := sess.Where("dashboard_id=?", cmd.DashboardId).Get(results) + if err != nil { + return err + } + + cmd.Id = results.Id + cmd.Updated = time.Now() + + println("exists", exist) + if exist { + + _, err = sess.ID(results.Id).Update(cmd) + } else { + _, err = sess.Insert(cmd) + } + + return err +} + +func GetProvisionedDashboardDataQuery(cmd *models.GetProvisionedDashboardDataQuery) error { + var result []*models.DashboardProvisioning + + if err := x.Where("name = ?", cmd.Name).Find(&result); err != nil { + return err + } + + cmd.Result = result + return nil +} diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/sqlstore/dashboard_provisioning_test.go new file mode 100644 index 00000000000..96e63d58933 --- /dev/null +++ b/pkg/services/sqlstore/dashboard_provisioning_test.go @@ -0,0 +1,48 @@ +package sqlstore + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestDashboardProvisioningTest(t *testing.T) { + Convey("Testing Dashboard provisioning", t, func() { + InitTestDB(t) + + saveDashboardCmd := &models.SaveDashboardCommand{ + OrgId: 1, + FolderId: 0, + IsFolder: false, + Dashboard: simplejson.NewFromAny(map[string]interface{}{ + "id": nil, + "title": "test dashboard", + }), + } + + Convey("Saving dashboards with extras", func() { + cmd := &models.SaveProvisionedDashboardCommand{ + DashboardCmd: saveDashboardCmd, + DashboardProvisioning: &models.DashboardProvisioning{ + Name: "default", + ExternalId: "/var/grafana.json", + }, + } + + err := SaveProvisionedDashboard(cmd) + So(err, ShouldBeNil) + So(cmd.Result, ShouldNotBeNil) + So(cmd.Result.Id, ShouldNotEqual, 0) + + Convey("Can query for provisioned dashboards", func() { + query := &models.GetProvisionedDashboardDataQuery{Name: "default"} + err := GetProvisionedDashboardDataQuery(query) + So(err, ShouldBeNil) + + So(len(query.Result), ShouldEqual, 1) + }) + }) + }) +} diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index 4f1602be931..ccaa4d013d3 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -150,4 +150,21 @@ func addDashboardMigration(mg *Migrator) { mg.AddMigration("Add column has_acl in dashboard", NewAddColumnMigration(dashboardV2, &Column{ Name: "has_acl", Type: DB_Bool, Nullable: false, Default: "0", })) + + dashboardExtrasTable := Table{ + Name: "dashboard_provisioning", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "dashboard_id", Type: DB_BigInt, Nullable: true}, + {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "external_id", Type: DB_Text, Nullable: false}, + {Name: "updated", Type: DB_Int, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"dashboard_id"}}, + {Cols: []string{"dashboard_id", "name"}, Type: IndexType}, + }, + } + + mg.AddMigration("create dashboard_provisioning", NewAddTableMigration(dashboardExtrasTable)) } diff --git a/pkg/services/sqlstore/playlist.go b/pkg/services/sqlstore/playlist.go index 72f3079db8d..4b4536455da 100644 --- a/pkg/services/sqlstore/playlist.go +++ b/pkg/services/sqlstore/playlist.go @@ -1,8 +1,6 @@ package sqlstore import ( - "fmt" - "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) @@ -27,8 +25,6 @@ func CreatePlaylist(cmd *m.CreatePlaylistCommand) error { _, err = x.Insert(&playlist) - fmt.Printf("%v", playlist.Id) - playlistItems := make([]m.PlaylistItem, 0) for _, item := range cmd.Items { playlistItems = append(playlistItems, m.PlaylistItem{ From 77a4ccb82269cc55439c14636c9c8e2257df211a Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 23 Jan 2018 20:22:04 +0100 Subject: [PATCH 003/110] dashboards: save provisioning meta data --- pkg/models/dashboard_provisioning.go | 1 - .../provisioning/dashboards/file_reader.go | 32 ++++++++++--------- .../sqlstore/dashboard_provisioning.go | 16 +++++----- .../sqlstore/dashboard_provisioning_test.go | 2 ++ 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/pkg/models/dashboard_provisioning.go b/pkg/models/dashboard_provisioning.go index d6a05a41090..2640e7f93ea 100644 --- a/pkg/models/dashboard_provisioning.go +++ b/pkg/models/dashboard_provisioning.go @@ -1,2 +1 @@ package models - diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 91aa6169390..3005de9a390 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -25,12 +25,12 @@ var ( ) type fileReader struct { - Cfg *DashboardsAsConfig - Path string - log log.Logger - dashboardRepo dashboards.Repository - cache *dashboardCache - createWalk func(fr *fileReader, folderId int64) filepath.WalkFunc + Cfg *DashboardsAsConfig + Path string + log log.Logger + dashboardRepo dashboards.Repository + cache *dashboardCache + createWalk func(fr *fileReader, folderId int64) filepath.WalkFunc } func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) { @@ -50,12 +50,12 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade } return &fileReader{ - Cfg: cfg, - Path: path, - log: log, - dashboardRepo: dashboards.GetRepository(), - cache: NewDashboardCache(), - createWalk: createWalkFn, + Cfg: cfg, + Path: path, + log: log, + dashboardRepo: dashboards.GetRepository(), + cache: NewDashboardCache(), + createWalk: createWalkFn, }, nil } @@ -209,9 +209,11 @@ func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc { } } func saveDashboard(fr *fileReader, path string, dash *dashboards.SaveDashboardDTO, modTime time.Time) error { - //dash.Extras["provisioning.filepath"] = path - _, err := fr.dashboardRepo.SaveDashboard(dash) - + d := &models.DashboardProvisioning{ + ExternalId: path, + Name: fr.Cfg.Name, + } + _, err := fr.dashboardRepo.SaveProvisionedDashboard(dash, d) return err } diff --git a/pkg/services/sqlstore/dashboard_provisioning.go b/pkg/services/sqlstore/dashboard_provisioning.go index 91d63e82a28..c064f78ce67 100644 --- a/pkg/services/sqlstore/dashboard_provisioning.go +++ b/pkg/services/sqlstore/dashboard_provisioning.go @@ -28,25 +28,25 @@ func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error } cmd.Result = cmd.DashboardCmd.Result - return saveProvionedData(sess, cmd.DashboardProvisioning) + + return saveProvionedData(sess, cmd.DashboardProvisioning, cmd.Result) }) } -func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning) error { - results := &models.DashboardProvisioning{} +func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error { + result := &models.DashboardProvisioning{} - exist, err := sess.Where("dashboard_id=?", cmd.DashboardId).Get(results) + exist, err := sess.Where("dashboard_id=?", dashboard.Id).Get(result) if err != nil { return err } - cmd.Id = results.Id + cmd.Id = result.Id cmd.Updated = time.Now() + cmd.DashboardId = dashboard.Id - println("exists", exist) if exist { - - _, err = sess.ID(results.Id).Update(cmd) + _, err = sess.ID(result.Id).Update(cmd) } else { _, err = sess.Insert(cmd) } diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/sqlstore/dashboard_provisioning_test.go index 96e63d58933..c42d95e7495 100644 --- a/pkg/services/sqlstore/dashboard_provisioning_test.go +++ b/pkg/services/sqlstore/dashboard_provisioning_test.go @@ -35,6 +35,7 @@ func TestDashboardProvisioningTest(t *testing.T) { So(err, ShouldBeNil) So(cmd.Result, ShouldNotBeNil) So(cmd.Result.Id, ShouldNotEqual, 0) + dashId := cmd.Result.Id Convey("Can query for provisioned dashboards", func() { query := &models.GetProvisionedDashboardDataQuery{Name: "default"} @@ -42,6 +43,7 @@ func TestDashboardProvisioningTest(t *testing.T) { So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) + So(query.Result[0].DashboardId, ShouldEqual, dashId) }) }) }) From 57e7048b8ffcee67d241c930a4f43ff001e63b35 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 24 Jan 2018 15:22:03 +0100 Subject: [PATCH 004/110] provisioning: enables title changes for dashboards --- .../provisioning/dashboards/file_reader.go | 64 +++++++++++++++---- .../dashboards/file_reader_test.go | 6 +- pkg/services/sqlstore/dashboard.go | 1 - 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 3005de9a390..dbbb44f15b8 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -30,7 +30,7 @@ type fileReader struct { log log.Logger dashboardRepo dashboards.Repository cache *dashboardCache - createWalk func(fr *fileReader, folderId int64) filepath.WalkFunc + createWalk func(fr *fileReader, folderId int64, provisionedDashboards map[string]*models.DashboardProvisioning) filepath.WalkFunc } func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) { @@ -98,7 +98,26 @@ func (fr *fileReader) startWalkingDisk() error { return err } - return filepath.Walk(fr.Path, fr.createWalk(fr, folderId)) + byPath, err := getProvisionedDashboardByPath(fr.dashboardRepo, fr.Cfg.Name) + if err != nil { + return err + } + + return filepath.Walk(fr.Path, fr.createWalk(fr, folderId, byPath)) +} + +func getProvisionedDashboardByPath(repo dashboards.Repository, name string) (map[string]*models.DashboardProvisioning, error) { + arr, err := repo.GetProvisionedDashboardData(name) + if err != nil { + return nil, err + } + + byPath := map[string]*models.DashboardProvisioning{} + for _, pd := range arr { + byPath[pd.ExternalId] = pd + } + + return byPath, nil } func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (int64, error) { @@ -150,7 +169,7 @@ func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) { return fileinfo, err } -func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc { +func createWalkFn(fr *fileReader, folderId int64, provisionedDashboards map[string]*models.DashboardProvisioning) filepath.WalkFunc { return func(path string, fileInfo os.FileInfo, err error) error { if err != nil { return err @@ -177,18 +196,31 @@ func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc { return nil } - if dash.Dashboard.Id != 0 { - fr.log.Error("Cannot provision dashboard. Please remove the id property from the json file") - return nil + var dbDashboard *models.Dashboard + cmd := &models.GetDashboardQuery{} + provisionedData, allReadyProvisioned := provisionedDashboards[path] + + // see if the + if allReadyProvisioned { + dash.Dashboard.Id = provisionedData.DashboardId + dash.Dashboard.Data.Set("id", provisionedData.DashboardId) + cmd.Id = provisionedData.DashboardId + } else { + if dash.Dashboard.Id != 0 { + fr.log.Error("Cannot provision dashboard. Please remove the id property from the json file") + return nil + } + + cmd.Slug = dash.Dashboard.Slug } - cmd := &models.GetDashboardQuery{Slug: dash.Dashboard.Slug} err = bus.Dispatch(cmd) + dbDashboard = cmd.Result // if we don't have the dashboard in the db, save it! if err == models.ErrDashboardNotFound { fr.log.Debug("saving new dashboard", "file", path) - err = saveDashboard(fr, path, dash, fileInfo.ModTime()) + err = saveDashboard(fr, path, dash) return err } @@ -198,24 +230,30 @@ func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc { } // break if db version is newer then fil version - if cmd.Result.Updated.Unix() >= resolvedFileInfo.ModTime().Unix() { + if dbDashboard.Updated.Unix() >= resolvedFileInfo.ModTime().Unix() { return nil } fr.log.Debug("loading dashboard from disk into database.", "file", path) - err = saveDashboard(fr, path, dash, fileInfo.ModTime()) + err = saveDashboard(fr, path, dash) return err } } -func saveDashboard(fr *fileReader, path string, dash *dashboards.SaveDashboardDTO, modTime time.Time) error { + +func saveDashboard(fr *fileReader, path string, dash *dashboards.SaveDashboardDTO) error { d := &models.DashboardProvisioning{ ExternalId: path, Name: fr.Cfg.Name, } - _, err := fr.dashboardRepo.SaveProvisionedDashboard(dash, d) - return err + _, err := fr.dashboardRepo.SaveProvisionedDashboard(dash, d) + if err != nil { + return err + } + + fr.cache.addDashboardCache(path, dash) + return nil } func validateWalkablePath(fileInfo os.FileInfo) (bool, error) { diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index 5c90e573f80..390190e29af 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -174,13 +174,15 @@ func TestDashboardFileReader(t *testing.T) { reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) So(err, ShouldBeNil) + emptyProvisioned := map[string]*models.DashboardProvisioning{} + Convey("should skip dirs that starts with .", func() { - shouldSkip := reader.createWalk(reader, 0)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil) + shouldSkip := reader.createWalk(reader, 0, emptyProvisioned)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil) So(shouldSkip, ShouldEqual, filepath.SkipDir) }) Convey("should keep walking if file is not .json", func() { - shouldSkip := reader.createWalk(reader, 0)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil) + shouldSkip := reader.createWalk(reader, 0, emptyProvisioned)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil) So(shouldSkip, ShouldBeNil) }) }) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 4c72e3423be..2b94f0e089c 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -31,7 +31,6 @@ func saveDashboard(sess *DBSession, cmd *m.SaveDashboardCommand) error { // try get existing dashboard var existing, sameTitle m.Dashboard - if dash.Id > 0 { dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.Id, dash.OrgId).Get(&existing) if err != nil { From d62d5c74186ff3398660cf643c8bb176466c7381 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 25 Jan 2018 15:25:07 +0100 Subject: [PATCH 005/110] provisioning: delete dashboards from db when file is missing --- pkg/models/dashboards.go | 5 ++ .../provisioning/dashboards/file_reader.go | 46 ++++++++++++++----- .../dashboards/file_reader_test.go | 5 +- pkg/services/sqlstore/dashboard.go | 1 + 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 577e97c657d..9e024d2db5e 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -58,6 +58,11 @@ type Dashboard struct { Data *simplejson.Json } +func (d *Dashboard) SetId(id int64) { + d.Id = id + d.Data.Set("id", id) +} + // NewDashboard creates a new dashboard func NewDashboard(title string) *Dashboard { dash := &Dashboard{} diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index dbbb44f15b8..085c6173df4 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -30,7 +30,7 @@ type fileReader struct { log log.Logger dashboardRepo dashboards.Repository cache *dashboardCache - createWalk func(fr *fileReader, folderId int64, provisionedDashboards map[string]*models.DashboardProvisioning) filepath.WalkFunc + createWalk func(fr *fileReader, folderId int64, provisionedDashboards map[string]*models.DashboardProvisioning, filesOnDisk map[string]bool) filepath.WalkFunc } func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) { @@ -103,7 +103,29 @@ func (fr *fileReader) startWalkingDisk() error { return err } - return filepath.Walk(fr.Path, fr.createWalk(fr, folderId, byPath)) + filesFoundOnDisk := map[string]bool{} + + err = filepath.Walk(fr.Path, fr.createWalk(fr, folderId, byPath, filesFoundOnDisk)) + + //delete dashboards without files + var dashboardToDelete []int64 + for path, provisioningData := range byPath { + _, existsInDatabase := filesFoundOnDisk[path] + if !existsInDatabase { + dashboardToDelete = append(dashboardToDelete, provisioningData.DashboardId) + } + } + + for _, dashboardId := range dashboardToDelete { + fr.log.Debug("deleting provisioned dashboard. missing on disk", "id", dashboardId) + cmd := &models.DeleteDashboardCommand{OrgId: fr.Cfg.OrgId, Id: dashboardId} + err := bus.Dispatch(cmd) + if err != nil { + return err + } + } + + return nil } func getProvisionedDashboardByPath(repo dashboards.Repository, name string) (map[string]*models.DashboardProvisioning, error) { @@ -169,7 +191,7 @@ func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) { return fileinfo, err } -func createWalkFn(fr *fileReader, folderId int64, provisionedDashboards map[string]*models.DashboardProvisioning) filepath.WalkFunc { +func createWalkFn(fr *fileReader, folderId int64, provisionedDashboards map[string]*models.DashboardProvisioning, filesOnDisk map[string]bool) filepath.WalkFunc { return func(path string, fileInfo os.FileInfo, err error) error { if err != nil { return err @@ -185,6 +207,9 @@ func createWalkFn(fr *fileReader, folderId int64, provisionedDashboards map[stri return err } + // mark file as provisioned + filesOnDisk[path] = true + cachedDashboard, exist := fr.cache.getCache(path) if exist && cachedDashboard.UpdatedAt == resolvedFileInfo.ModTime() { return nil @@ -197,25 +222,24 @@ func createWalkFn(fr *fileReader, folderId int64, provisionedDashboards map[stri } var dbDashboard *models.Dashboard - cmd := &models.GetDashboardQuery{} + query := &models.GetDashboardQuery{} provisionedData, allReadyProvisioned := provisionedDashboards[path] - // see if the if allReadyProvisioned { - dash.Dashboard.Id = provisionedData.DashboardId - dash.Dashboard.Data.Set("id", provisionedData.DashboardId) - cmd.Id = provisionedData.DashboardId + dash.Dashboard.SetId(provisionedData.DashboardId) + + query.Id = provisionedData.DashboardId } else { if dash.Dashboard.Id != 0 { fr.log.Error("Cannot provision dashboard. Please remove the id property from the json file") return nil } - cmd.Slug = dash.Dashboard.Slug + query.Slug = dash.Dashboard.Slug } - err = bus.Dispatch(cmd) - dbDashboard = cmd.Result + err = bus.Dispatch(query) + dbDashboard = query.Result // if we don't have the dashboard in the db, save it! if err == models.ErrDashboardNotFound { diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index 390190e29af..cd7d9a03fd3 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -175,14 +175,15 @@ func TestDashboardFileReader(t *testing.T) { So(err, ShouldBeNil) emptyProvisioned := map[string]*models.DashboardProvisioning{} + noFiles := map[string]bool{} Convey("should skip dirs that starts with .", func() { - shouldSkip := reader.createWalk(reader, 0, emptyProvisioned)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil) + shouldSkip := reader.createWalk(reader, 0, emptyProvisioned, noFiles)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil) So(shouldSkip, ShouldEqual, filepath.SkipDir) }) Convey("should keep walking if file is not .json", func() { - shouldSkip := reader.createWalk(reader, 0, emptyProvisioned)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil) + shouldSkip := reader.createWalk(reader, 0, emptyProvisioned, noFiles)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil) So(shouldSkip, ShouldBeNil) }) }) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 2b94f0e089c..bc60165ca69 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -313,6 +313,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { "DELETE FROM dashboard_version WHERE dashboard_id = ?", "DELETE FROM dashboard WHERE folder_id = ?", "DELETE FROM annotation WHERE dashboard_id = ?", + "DELETE FROM dashboard_provisioning WHERE dashboard_id = ?", } for _, sql := range deletes { From b84fd3a7aea83f894bcc2dc9246507fc84aa49fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 7 Feb 2018 17:54:21 +0100 Subject: [PATCH 006/110] fix: initial fix for #10822 --- pkg/services/sqlstore/dashboard.go | 1 + pkg/services/sqlstore/search_builder.go | 22 ++---------- pkg/services/sqlstore/sqlbuilder.go | 45 +++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 pkg/services/sqlstore/sqlbuilder.go diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index be8b11b1f5b..c187360dc33 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -279,6 +279,7 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear var res []DashboardSearchProjection sql, params := sb.ToSql() + sqlog.Info("sql", "sql", sql, "params", params) err := x.Sql(sql, params...).Find(&res) if err != nil { return nil, err diff --git a/pkg/services/sqlstore/search_builder.go b/pkg/services/sqlstore/search_builder.go index 627074d5453..91e2742e165 100644 --- a/pkg/services/sqlstore/search_builder.go +++ b/pkg/services/sqlstore/search_builder.go @@ -1,7 +1,6 @@ package sqlstore import ( - "bytes" "strings" m "github.com/grafana/grafana/pkg/models" @@ -9,6 +8,7 @@ import ( // SearchBuilder is a builder/object mother that builds a dashboard search query type SearchBuilder struct { + SqlBuilder tags []string isStarred bool limit int @@ -18,8 +18,6 @@ type SearchBuilder struct { whereTypeFolder bool whereTypeDash bool whereFolderIds []int64 - sql bytes.Buffer - params []interface{} } func NewSearchBuilder(signedInUser *m.SignedInUser, limit int) *SearchBuilder { @@ -176,23 +174,7 @@ func (sb *SearchBuilder) buildSearchWhereClause() { } } - if sb.signedInUser.OrgRole != m.ROLE_ADMIN { - allowedDashboardsSubQuery := ` AND (dashboard.has_acl = ` + dialect.BooleanStr(false) + ` OR dashboard.id in ( - SELECT distinct d.id AS DashboardId - FROM dashboard AS d - LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id - LEFT JOIN team_member as ugm on ugm.team_id = da.team_id - LEFT JOIN org_user ou on ou.role = da.role - WHERE - d.has_acl = ` + dialect.BooleanStr(true) + ` and - (da.user_id = ? or ugm.user_id = ? or ou.id is not null) - and d.org_id = ? - ) - )` - - sb.sql.WriteString(allowedDashboardsSubQuery) - sb.params = append(sb.params, sb.signedInUser.UserId, sb.signedInUser.UserId, sb.signedInUser.OrgId) - } + sb.writeDashboardPermissionFilter(sb.signedInUser, m.PERMISSION_VIEW) if len(sb.whereTitle) > 0 { sb.sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?") diff --git a/pkg/services/sqlstore/sqlbuilder.go b/pkg/services/sqlstore/sqlbuilder.go new file mode 100644 index 00000000000..9a0dd0d3989 --- /dev/null +++ b/pkg/services/sqlstore/sqlbuilder.go @@ -0,0 +1,45 @@ +package sqlstore + +import ( + "bytes" + "strings" + + m "github.com/grafana/grafana/pkg/models" +) + +type SqlBuilder struct { + sql bytes.Buffer + params []interface{} +} + +func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, minPermission m.PermissionType) { + + if user.OrgRole == m.ROLE_ADMIN { + return + } + + okRoles := []interface{}{user.OrgRole} + + if user.OrgRole == m.ROLE_EDITOR { + okRoles = append(okRoles, m.ROLE_VIEWER) + } + + sb.sql.WriteString(` AND + ( + dashboard.has_acl = ` + dialect.BooleanStr(false) + ` OR + dashboard.id in ( + SELECT distinct d.id AS DashboardId + FROM dashboard AS d + LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id + LEFT JOIN team_member as ugm on ugm.team_id = da.team_id + WHERE + d.has_acl = ` + dialect.BooleanStr(true) + ` AND + d.org_id = ? AND + da.permission >= ? AND + (da.user_id = ? or ugm.user_id = ? or da.role IN (?` + strings.Repeat(",?", len(okRoles)-1) + `)) + ) + )`) + + sb.params = append(sb.params, user.OrgId, minPermission, user.UserId, user.UserId) + sb.params = append(sb.params, okRoles...) +} From e0abd862e0714a060badd7c13e2a12c7b8b5915d Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 8 Feb 2018 16:36:53 +0100 Subject: [PATCH 007/110] provisioning: fixed bug in saving dashboards. --- pkg/services/dashboards/dashboards.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/dashboards/dashboards.go b/pkg/services/dashboards/dashboards.go index ce60fde0454..b0392f7944f 100644 --- a/pkg/services/dashboards/dashboards.go +++ b/pkg/services/dashboards/dashboards.go @@ -124,7 +124,7 @@ func (dr *DashboardRepository) SaveDashboard(dto *SaveDashboardDTO) (*models.Das return nil, err } - err = bus.Dispatch(&cmd) + err = bus.Dispatch(cmd) if err != nil { return nil, err } From 8e8f3c4332fa6effd3111be5bb83178e64e44def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 8 Feb 2018 17:11:01 +0100 Subject: [PATCH 008/110] dashboard and folder search with permissions --- pkg/api/api.go | 2 - pkg/api/dashboard.go | 16 ------ pkg/api/search.go | 7 +++ pkg/models/dashboards.go | 12 ----- pkg/services/search/handlers.go | 1 + pkg/services/search/models.go | 3 +- pkg/services/sqlstore/dashboard.go | 53 ++----------------- .../sqlstore/dashboard_folder_test.go | 26 +++++---- pkg/services/sqlstore/search_builder.go | 6 ++- pkg/services/sqlstore/search_builder_test.go | 3 +- pkg/services/sqlstore/sqlbuilder.go | 4 +- .../dashboard/folder_picker/folder_picker.ts | 8 ++- 12 files changed, 43 insertions(+), 98 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 752af7602f5..793f5a2e830 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -261,8 +261,6 @@ func (hs *HttpServer) registerRoutes() { dashboardRoute.Get("/tags", GetDashboardTags) dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard)) - dashboardRoute.Get("/folders", wrap(GetFoldersForSignedInUser)) - dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) { dashIdRoute.Get("/versions", wrap(GetDashboardVersions)) dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index d7676899eb2..a1f3560c0c6 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -490,19 +490,3 @@ func GetDashboardTags(c *middleware.Context) { c.JSON(200, query.Result) } - -func GetFoldersForSignedInUser(c *middleware.Context) Response { - title := c.Query("query") - query := m.GetFoldersForSignedInUserQuery{ - OrgId: c.OrgId, - SignedInUser: c.SignedInUser, - Title: title, - } - - err := bus.Dispatch(&query) - if err != nil { - return ApiError(500, "Failed to get folders from database", err) - } - - return Json(200, query.Result) -} diff --git a/pkg/api/search.go b/pkg/api/search.go index fee062a5599..f79385d83f8 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" ) @@ -15,11 +16,16 @@ func Search(c *middleware.Context) { starred := c.Query("starred") limit := c.QueryInt("limit") dashboardType := c.Query("type") + permission := models.PERMISSION_VIEW if limit == 0 { limit = 1000 } + if c.Query("permission") == "Edit" { + permission = models.PERMISSION_EDIT + } + dbids := make([]int64, 0) for _, id := range c.QueryStrings("dashboardIds") { dashboardId, err := strconv.ParseInt(id, 10, 64) @@ -46,6 +52,7 @@ func Search(c *middleware.Context) { DashboardIds: dbids, Type: dashboardType, FolderIds: folderIds, + Permission: permission, } err := bus.Dispatch(&searchQuery) diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 12216718b44..a91d4c4ed62 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -270,18 +270,6 @@ type GetDashboardsBySlugQuery struct { Result []*Dashboard } -type GetFoldersForSignedInUserQuery struct { - OrgId int64 - SignedInUser *SignedInUser - Title string - Result []*DashboardFolder -} - -type DashboardFolder struct { - Id int64 `json:"id"` - Title string `json:"title"` -} - type DashboardPermissionForUser struct { DashboardId int64 `json:"dashboardId"` Permission PermissionType `json:"permission"` diff --git a/pkg/services/search/handlers.go b/pkg/services/search/handlers.go index 247585402ef..cf194c320bb 100644 --- a/pkg/services/search/handlers.go +++ b/pkg/services/search/handlers.go @@ -21,6 +21,7 @@ func searchHandler(query *Query) error { FolderIds: query.FolderIds, Tags: query.Tags, Limit: query.Limit, + Permission: query.Permission, } if err := bus.Dispatch(&dashQuery); err != nil { diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go index 6dea975d9fe..2da09672f13 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -52,6 +52,7 @@ type Query struct { Type string DashboardIds []int64 FolderIds []int64 + Permission models.PermissionType Result HitList } @@ -66,7 +67,7 @@ type FindPersistedDashboardsQuery struct { FolderIds []int64 Tags []string Limit int - IsBrowse bool + Permission models.PermissionType Result HitList } diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index c187360dc33..dd739343d7b 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -1,6 +1,7 @@ package sqlstore import ( + "fmt" "strings" "time" @@ -21,7 +22,6 @@ func init() { bus.AddHandler("sql", GetDashboardSlugById) bus.AddHandler("sql", GetDashboardUIDById) bus.AddHandler("sql", GetDashboardsByPluginId) - bus.AddHandler("sql", GetFoldersForSignedInUser) bus.AddHandler("sql", GetDashboardPermissionsForUser) bus.AddHandler("sql", GetDashboardsBySlug) } @@ -256,7 +256,7 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear limit = 1000 } - sb := NewSearchBuilder(query.SignedInUser, limit). + sb := NewSearchBuilder(query.SignedInUser, limit, query.Permission). WithTags(query.Tags). WithDashboardIdsIn(query.DashboardIds) @@ -279,6 +279,7 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear var res []DashboardSearchProjection sql, params := sb.ToSql() + fmt.Printf("%s, %v", sql, params) sqlog.Info("sql", "sql", sql, "params", params) err := x.Sql(sql, params...).Find(&res) if err != nil { @@ -358,54 +359,6 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error { return err } -func GetFoldersForSignedInUser(query *m.GetFoldersForSignedInUserQuery) error { - query.Result = make([]*m.DashboardFolder, 0) - var err error - - if query.SignedInUser.OrgRole == m.ROLE_ADMIN { - sql := `SELECT distinct d.id, d.title - FROM dashboard AS d WHERE d.is_folder = ? AND d.org_id = ? - ORDER BY d.title ASC` - - err = x.Sql(sql, dialect.BooleanStr(true), query.OrgId).Find(&query.Result) - } else { - params := make([]interface{}, 0) - sql := `SELECT distinct d.id, d.title - FROM dashboard AS d - LEFT JOIN dashboard_acl AS da ON d.id = da.dashboard_id - LEFT JOIN team_member AS ugm ON ugm.team_id = da.team_id - LEFT JOIN org_user ou ON ou.role = da.role AND ou.user_id = ? - LEFT JOIN org_user ouRole ON ouRole.role = 'Editor' AND ouRole.user_id = ? AND ouRole.org_id = ?` - params = append(params, query.SignedInUser.UserId) - params = append(params, query.SignedInUser.UserId) - params = append(params, query.OrgId) - - sql += ` WHERE - d.org_id = ? AND - d.is_folder = ? AND - ( - (d.has_acl = ? AND da.permission > 1 AND (da.user_id = ? OR ugm.user_id = ? OR ou.id IS NOT NULL)) - OR (d.has_acl = ? AND ouRole.id IS NOT NULL) - )` - params = append(params, query.OrgId) - params = append(params, dialect.BooleanStr(true)) - params = append(params, dialect.BooleanStr(true)) - params = append(params, query.SignedInUser.UserId) - params = append(params, query.SignedInUser.UserId) - params = append(params, dialect.BooleanStr(false)) - - if len(query.Title) > 0 { - sql += " AND d.title " + dialect.LikeStr() + " ?" - params = append(params, "%"+query.Title+"%") - } - - sql += ` ORDER BY d.title ASC` - err = x.Sql(sql, params...).Find(&query.Result) - } - - return err -} - func DeleteDashboard(cmd *m.DeleteDashboardCommand) error { return inTransaction(func(sess *DBSession) error { dashboard := m.Dashboard{Id: cmd.Id, OrgId: cmd.OrgId} diff --git a/pkg/services/sqlstore/dashboard_folder_test.go b/pkg/services/sqlstore/dashboard_folder_test.go index 4818deaae14..bd09e7490cb 100644 --- a/pkg/services/sqlstore/dashboard_folder_test.go +++ b/pkg/services/sqlstore/dashboard_folder_test.go @@ -227,12 +227,14 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("Admin users", func() { Convey("Should have write access to all dashboard folders in their org", func() { - query := m.GetFoldersForSignedInUserQuery{ + query := search.FindPersistedDashboardsQuery{ OrgId: 1, - SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN}, + SignedInUser: &m.SignedInUser{UserId: adminUser.Id, OrgRole: m.ROLE_ADMIN, OrgId: 1}, + Permission: m.PERMISSION_VIEW, + Type: "dash-folder", } - err := GetFoldersForSignedInUser(&query) + err := SearchDashboards(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 2) @@ -260,13 +262,14 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("Editor users", func() { - query := m.GetFoldersForSignedInUserQuery{ + query := search.FindPersistedDashboardsQuery{ OrgId: 1, - SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgRole: m.ROLE_EDITOR}, + SignedInUser: &m.SignedInUser{UserId: editorUser.Id, OrgRole: m.ROLE_EDITOR, OrgId: 1}, + Permission: m.PERMISSION_EDIT, } Convey("Should have write access to all dashboard folders with default ACL", func() { - err := GetFoldersForSignedInUser(&query) + err := SearchDashboards(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 2) @@ -295,7 +298,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("Should have write access to one dashboard folder if default role changed to view for one folder", func() { updateTestDashboardWithAcl(folder1.Id, editorUser.Id, m.PERMISSION_VIEW) - err := GetFoldersForSignedInUser(&query) + err := SearchDashboards(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) @@ -305,13 +308,14 @@ func TestDashboardFolderDataAccess(t *testing.T) { }) Convey("Viewer users", func() { - query := m.GetFoldersForSignedInUserQuery{ + query := search.FindPersistedDashboardsQuery{ OrgId: 1, - SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgRole: m.ROLE_VIEWER}, + SignedInUser: &m.SignedInUser{UserId: viewerUser.Id, OrgRole: m.ROLE_VIEWER, OrgId: 1}, + Permission: m.PERMISSION_EDIT, } Convey("Should have no write access to any dashboard folders with default ACL", func() { - err := GetFoldersForSignedInUser(&query) + err := SearchDashboards(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 0) @@ -338,7 +342,7 @@ func TestDashboardFolderDataAccess(t *testing.T) { Convey("Should be able to get one dashboard folder if default role changed to edit for one folder", func() { updateTestDashboardWithAcl(folder1.Id, viewerUser.Id, m.PERMISSION_EDIT) - err := GetFoldersForSignedInUser(&query) + err := SearchDashboards(&query) So(err, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) diff --git a/pkg/services/sqlstore/search_builder.go b/pkg/services/sqlstore/search_builder.go index 91e2742e165..0db0fca53b0 100644 --- a/pkg/services/sqlstore/search_builder.go +++ b/pkg/services/sqlstore/search_builder.go @@ -18,12 +18,14 @@ type SearchBuilder struct { whereTypeFolder bool whereTypeDash bool whereFolderIds []int64 + permission m.PermissionType } -func NewSearchBuilder(signedInUser *m.SignedInUser, limit int) *SearchBuilder { +func NewSearchBuilder(signedInUser *m.SignedInUser, limit int, permission m.PermissionType) *SearchBuilder { searchBuilder := &SearchBuilder{ signedInUser: signedInUser, limit: limit, + permission: permission, } return searchBuilder @@ -174,7 +176,7 @@ func (sb *SearchBuilder) buildSearchWhereClause() { } } - sb.writeDashboardPermissionFilter(sb.signedInUser, m.PERMISSION_VIEW) + sb.writeDashboardPermissionFilter(sb.signedInUser, sb.permission) if len(sb.whereTitle) > 0 { sb.sql.WriteString(" AND dashboard.title " + dialect.LikeStr() + " ?") diff --git a/pkg/services/sqlstore/search_builder_test.go b/pkg/services/sqlstore/search_builder_test.go index 32ccbc583f5..e8b02c445ec 100644 --- a/pkg/services/sqlstore/search_builder_test.go +++ b/pkg/services/sqlstore/search_builder_test.go @@ -16,7 +16,8 @@ func TestSearchBuilder(t *testing.T) { OrgId: 1, UserId: 1, } - sb := NewSearchBuilder(signedInUser, 1000) + + sb := NewSearchBuilder(signedInUser, 1000, m.PERMISSION_VIEW) Convey("When building a normal search", func() { sql, params := sb.IsStarred().WithTitle("test").ToSql() diff --git a/pkg/services/sqlstore/sqlbuilder.go b/pkg/services/sqlstore/sqlbuilder.go index 9a0dd0d3989..6274458818e 100644 --- a/pkg/services/sqlstore/sqlbuilder.go +++ b/pkg/services/sqlstore/sqlbuilder.go @@ -12,7 +12,7 @@ type SqlBuilder struct { params []interface{} } -func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, minPermission m.PermissionType) { +func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, permission m.PermissionType) { if user.OrgRole == m.ROLE_ADMIN { return @@ -40,6 +40,6 @@ func (sb *SqlBuilder) writeDashboardPermissionFilter(user *m.SignedInUser, minPe ) )`) - sb.params = append(sb.params, user.OrgId, minPermission, user.UserId, user.UserId) + sb.params = append(sb.params, user.OrgId, permission, user.UserId, user.UserId) sb.params = append(sb.params, okRoles...) } diff --git a/public/app/features/dashboard/folder_picker/folder_picker.ts b/public/app/features/dashboard/folder_picker/folder_picker.ts index 56284a877c5..0e5c22c4db2 100644 --- a/public/app/features/dashboard/folder_picker/folder_picker.ts +++ b/public/app/features/dashboard/folder_picker/folder_picker.ts @@ -30,7 +30,13 @@ export class FolderPickerCtrl { } getOptions(query) { - return this.backendSrv.get('api/dashboards/folders', { query: query }).then(result => { + const params = { + query: query, + type: 'dash-folder', + permission: 'Edit', + }; + + return this.backendSrv.get('api/search', params).then(result => { if ( query === '' || query.toLowerCase() === 'g' || From 8921b0b51703ef3c889f8a6682d50cda1f941664 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 8 Feb 2018 19:16:45 +0100 Subject: [PATCH 009/110] db test: allow use of env variable for database engine to run tests for --- pkg/services/sqlstore/datasource_test.go | 31 +++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/pkg/services/sqlstore/datasource_test.go b/pkg/services/sqlstore/datasource_test.go index e6f0114ab4d..28f5b8eba9d 100644 --- a/pkg/services/sqlstore/datasource_test.go +++ b/pkg/services/sqlstore/datasource_test.go @@ -1,6 +1,8 @@ package sqlstore import ( + "os" + "strings" "testing" "github.com/go-xorm/xorm" @@ -11,10 +13,33 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" ) +var ( + dbSqlite = "sqlite" + dbMySql = "mysql" + dbPostgres = "postgres" +) + func InitTestDB(t *testing.T) *xorm.Engine { - x, err := xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr) - //x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr) - //x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr) + selectedDb := dbSqlite + //selectedDb := dbMySql + //selectedDb := dbPostgres + + var x *xorm.Engine + var err error + + // environment variable present for test db? + if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present { + selectedDb = db + } + + switch strings.ToLower(selectedDb) { + case dbMySql: + x, err = xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr) + case dbPostgres: + x, err = xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr) + default: + x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr) + } // x.ShowSQL() From a86f2fa34b9ccd2e3f573238199200a53e9cf3b3 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 9 Feb 2018 10:42:37 +0100 Subject: [PATCH 010/110] user picker should only include users from current org (#10845) --- pkg/api/org_users.go | 18 +++++++------ pkg/models/org_user.go | 5 +++- pkg/services/sqlstore/org_test.go | 25 +++++++++++++++++++ pkg/services/sqlstore/org_users.go | 24 +++++++++++++++++- .../app/core/components/Picker/UserPicker.tsx | 8 +++--- 5 files changed, 67 insertions(+), 13 deletions(-) diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 57a15bd8db5..433b9f2bd66 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -46,26 +46,30 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response { // GET /api/org/users func GetOrgUsersForCurrentOrg(c *middleware.Context) Response { - return getOrgUsersHelper(c.OrgId) + return getOrgUsersHelper(c.OrgId, c.Params("query"), c.ParamsInt("limit")) } // GET /api/orgs/:orgId/users func GetOrgUsers(c *middleware.Context) Response { - return getOrgUsersHelper(c.ParamsInt64(":orgId")) + return getOrgUsersHelper(c.ParamsInt64(":orgId"), "", 0) } -func getOrgUsersHelper(orgId int64) Response { - query := m.GetOrgUsersQuery{OrgId: orgId} +func getOrgUsersHelper(orgId int64, query string, limit int) Response { + q := m.GetOrgUsersQuery{ + OrgId: orgId, + Query: query, + Limit: limit, + } - if err := bus.Dispatch(&query); err != nil { + if err := bus.Dispatch(&q); err != nil { return ApiError(500, "Failed to get account user", err) } - for _, user := range query.Result { + for _, user := range q.Result { user.AvatarUrl = dtos.GetGravatarUrl(user.Email) } - return Json(200, query.Result) + return Json(200, q.Result) } // PATCH /api/org/users/:userId diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go index 9379625d458..ca32cc50060 100644 --- a/pkg/models/org_user.go +++ b/pkg/models/org_user.go @@ -95,7 +95,10 @@ type UpdateOrgUserCommand struct { // QUERIES type GetOrgUsersQuery struct { - OrgId int64 + OrgId int64 + Query string + Limit int + Result []*OrgUserDTO } diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index 59d96c4f8ca..5322dfd4748 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -123,6 +123,31 @@ func TestAccountDataAccess(t *testing.T) { So(query.Result[0].Role, ShouldEqual, "Admin") }) + Convey("Can get organization users with query", func() { + query := m.GetOrgUsersQuery{ + OrgId: ac1.OrgId, + Query: "ac1", + } + err := GetOrgUsers(&query) + + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 1) + So(query.Result[0].Email, ShouldEqual, ac1.Email) + }) + + Convey("Can get organization users with query and limit", func() { + query := m.GetOrgUsersQuery{ + OrgId: ac1.OrgId, + Query: "ac", + Limit: 1, + } + err := GetOrgUsers(&query) + + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 1) + So(query.Result[0].Email, ShouldEqual, ac1.Email) + }) + Convey("Can set using org", func() { cmd := m.SetUsingOrgCommand{UserId: ac2.Id, OrgId: ac1.Id} err := SetUsingOrg(&cmd) diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index 2c2a51fd362..79745c30386 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -2,6 +2,7 @@ package sqlstore import ( "fmt" + "strings" "time" "github.com/grafana/grafana/pkg/bus" @@ -69,9 +70,30 @@ func UpdateOrgUser(cmd *m.UpdateOrgUserCommand) error { func GetOrgUsers(query *m.GetOrgUsersQuery) error { query.Result = make([]*m.OrgUserDTO, 0) + sess := x.Table("org_user") sess.Join("INNER", "user", fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user"))) - sess.Where("org_user.org_id=?", query.OrgId) + + whereConditions := make([]string, 0) + whereParams := make([]interface{}, 0) + + whereConditions = append(whereConditions, "org_user.org_id = ?") + whereParams = append(whereParams, query.OrgId) + + if query.Query != "" { + queryWithWildcards := "%" + query.Query + "%" + whereConditions = append(whereConditions, "(user.email "+dialect.LikeStr()+" ? OR user.name "+dialect.LikeStr()+" ? OR user.login "+dialect.LikeStr()+" ?)") + whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards) + } + + if len(whereConditions) > 0 { + sess.Where(strings.Join(whereConditions, " AND "), whereParams...) + } + + if query.Limit > 0 { + sess.Limit(query.Limit, 0) + } + sess.Cols("org_user.org_id", "org_user.user_id", "user.email", "user.login", "org_user.role", "user.last_seen_at") sess.Asc("user.email", "user.login") diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index 5c36505aeaa..77bf6c1fe15 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -31,7 +31,7 @@ class UserPicker extends Component { this.debouncedSearch = debounce(this.search, 300, { leading: true, - trailing: false, + trailing: true, }); } @@ -39,10 +39,10 @@ class UserPicker extends Component { const { toggleLoading, backendSrv } = this.props; toggleLoading(true); - return backendSrv.get(`/api/users/search?perpage=10&page=1&query=${query}`).then(result => { - const users = result.users.map(user => { + return backendSrv.get(`/api/org/users?query=${query}&limit=10`).then(result => { + const users = result.map(user => { return { - id: user.id, + id: user.userId, label: `${user.login} - ${user.email}`, avatarUrl: user.avatarUrl, login: user.login, From 31f3fdcba39595340953508166f798b80956e621 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 9 Feb 2018 12:17:58 +0100 Subject: [PATCH 011/110] provisioning: delete dashboards before insert/update --- .../dashboards/dashboard_cache.go | 6 +- .../provisioning/dashboards/file_reader.go | 145 ++++++++++-------- .../dashboards/file_reader_test.go | 10 +- .../folder-one/dashboard1.json | 3 +- .../folder-one/dashboard2.json | 3 +- 5 files changed, 89 insertions(+), 78 deletions(-) diff --git a/pkg/services/provisioning/dashboards/dashboard_cache.go b/pkg/services/provisioning/dashboards/dashboard_cache.go index 7f8f6fe490f..4135a18be39 100644 --- a/pkg/services/provisioning/dashboards/dashboard_cache.go +++ b/pkg/services/provisioning/dashboards/dashboard_cache.go @@ -18,7 +18,11 @@ func (fr *dashboardCache) addDashboardCache(key string, json *dashboards.SaveDas fr.internalCache.Add(key, json, time.Minute*10) } -func (fr *dashboardCache) getCache(key string) (*dashboards.SaveDashboardDTO, bool) { +func (fr *dashboardCache) deleteDashboard(key string) { + fr.internalCache.Delete(key) +} + +func (fr *dashboardCache) getDashboard(key string) (*dashboards.SaveDashboardDTO, bool) { obj, exist := fr.internalCache.Get(key) if !exist { return nil, exist diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 085c6173df4..3dd35decc2c 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -30,7 +30,7 @@ type fileReader struct { log log.Logger dashboardRepo dashboards.Repository cache *dashboardCache - createWalk func(fr *fileReader, folderId int64, provisionedDashboards map[string]*models.DashboardProvisioning, filesOnDisk map[string]bool) filepath.WalkFunc + createWalk func(filesOnDisk map[string]os.FileInfo) filepath.WalkFunc } func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) { @@ -98,24 +98,26 @@ func (fr *fileReader) startWalkingDisk() error { return err } - byPath, err := getProvisionedDashboardByPath(fr.dashboardRepo, fr.Cfg.Name) + provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardRepo, fr.Cfg.Name) if err != nil { return err } - filesFoundOnDisk := map[string]bool{} + filesFoundOnDisk := map[string]os.FileInfo{} - err = filepath.Walk(fr.Path, fr.createWalk(fr, folderId, byPath, filesFoundOnDisk)) + err = filepath.Walk(fr.Path, fr.createWalk(filesFoundOnDisk)) - //delete dashboards without files + // find dashboards to delete since json file is missing var dashboardToDelete []int64 - for path, provisioningData := range byPath { + for path, provisioningData := range provisionedDashboardRefs { _, existsInDatabase := filesFoundOnDisk[path] if !existsInDatabase { dashboardToDelete = append(dashboardToDelete, provisioningData.DashboardId) + fr.cache.deleteDashboard(path) } } + // delete dashboard that are missing json file for _, dashboardId := range dashboardToDelete { fr.log.Debug("deleting provisioned dashboard. missing on disk", "id", dashboardId) cmd := &models.DeleteDashboardCommand{OrgId: fr.Cfg.OrgId, Id: dashboardId} @@ -125,9 +127,75 @@ func (fr *fileReader) startWalkingDisk() error { } } + // insert/update dashboards based on json files + for path, fileInfo := range filesFoundOnDisk { + err = fr.upsertDashboard(path, folderId, fileInfo, provisionedDashboardRefs) + if err != nil { + return err + } + } + return nil } +func (fr *fileReader) upsertDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) error { + resolvedFileInfo, err := resolveSymlink(fileInfo, path) + if err != nil { + return err + } + + cachedDashboard, exist := fr.cache.getDashboard(path) + if exist && cachedDashboard.UpdatedAt == resolvedFileInfo.ModTime() { + return nil + } + + dash, err := fr.readDashboardFromFile(path, folderId) + if err != nil { + fr.log.Error("failed to load dashboard from ", "file", path, "error", err) + return nil + } + + var dbDashboard *models.Dashboard + query := &models.GetDashboardQuery{} + provisionedData, allReadyProvisioned := provisionedDashboardRefs[path] + + if allReadyProvisioned { + dash.Dashboard.SetId(provisionedData.DashboardId) + + query.Id = provisionedData.DashboardId + } else { + if dash.Dashboard.Id != 0 { + fr.log.Error("Cannot provision dashboard. Please remove the id property from the json file") + return nil + } + + query.Slug = dash.Dashboard.Slug + } + + err = bus.Dispatch(query) + dbDashboard = query.Result + + // if we don't have the dashboard in the db, save it! + if err == models.ErrDashboardNotFound { + fr.log.Debug("saving new dashboard", "file", path) + err = saveDashboard(fr, path, dash) + return err + } + + if err != nil { + fr.log.Error("failed to query for dashboard", "slug", dash.Dashboard.Slug, "error", err) + return nil + } + + // break if db version is newer then fil version + if dbDashboard.Updated.Unix() >= resolvedFileInfo.ModTime().Unix() { + return nil + } + + fr.log.Debug("loading dashboard from disk into database.", "file", path) + return saveDashboard(fr, path, dash) +} + func getProvisionedDashboardByPath(repo dashboards.Repository, name string) (map[string]*models.DashboardProvisioning, error) { arr, err := repo.GetProvisionedDashboardData(name) if err != nil { @@ -191,7 +259,7 @@ func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) { return fileinfo, err } -func createWalkFn(fr *fileReader, folderId int64, provisionedDashboards map[string]*models.DashboardProvisioning, filesOnDisk map[string]bool) filepath.WalkFunc { +func createWalkFn(filesOnDisk map[string]os.FileInfo) filepath.WalkFunc { return func(path string, fileInfo os.FileInfo, err error) error { if err != nil { return err @@ -202,66 +270,8 @@ func createWalkFn(fr *fileReader, folderId int64, provisionedDashboards map[stri return err } - resolvedFileInfo, err := resolveSymlink(fileInfo, path) - if err != nil { - return err - } - - // mark file as provisioned - filesOnDisk[path] = true - - cachedDashboard, exist := fr.cache.getCache(path) - if exist && cachedDashboard.UpdatedAt == resolvedFileInfo.ModTime() { - return nil - } - - dash, err := fr.readDashboardFromFile(path, folderId) - if err != nil { - fr.log.Error("failed to load dashboard from ", "file", path, "error", err) - return nil - } - - var dbDashboard *models.Dashboard - query := &models.GetDashboardQuery{} - provisionedData, allReadyProvisioned := provisionedDashboards[path] - - if allReadyProvisioned { - dash.Dashboard.SetId(provisionedData.DashboardId) - - query.Id = provisionedData.DashboardId - } else { - if dash.Dashboard.Id != 0 { - fr.log.Error("Cannot provision dashboard. Please remove the id property from the json file") - return nil - } - - query.Slug = dash.Dashboard.Slug - } - - err = bus.Dispatch(query) - dbDashboard = query.Result - - // if we don't have the dashboard in the db, save it! - if err == models.ErrDashboardNotFound { - fr.log.Debug("saving new dashboard", "file", path) - err = saveDashboard(fr, path, dash) - return err - } - - if err != nil { - fr.log.Error("failed to query for dashboard", "slug", dash.Dashboard.Slug, "error", err) - return nil - } - - // break if db version is newer then fil version - if dbDashboard.Updated.Unix() >= resolvedFileInfo.ModTime().Unix() { - return nil - } - - fr.log.Debug("loading dashboard from disk into database.", "file", path) - err = saveDashboard(fr, path, dash) - - return err + filesOnDisk[path] = fileInfo + return nil } } @@ -276,7 +286,6 @@ func saveDashboard(fr *fileReader, path string, dash *dashboards.SaveDashboardDT return err } - fr.cache.addDashboardCache(path, dash) return nil } diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index cd7d9a03fd3..1e1f2df1e78 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -62,8 +62,8 @@ func TestDashboardFileReader(t *testing.T) { } } - So(dashboards, ShouldEqual, 2) So(folders, ShouldEqual, 1) + So(dashboards, ShouldEqual, 2) }) Convey("Should not update dashboards when db is newer", func() { @@ -174,16 +174,15 @@ func TestDashboardFileReader(t *testing.T) { reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) So(err, ShouldBeNil) - emptyProvisioned := map[string]*models.DashboardProvisioning{} - noFiles := map[string]bool{} + noFiles := map[string]os.FileInfo{} Convey("should skip dirs that starts with .", func() { - shouldSkip := reader.createWalk(reader, 0, emptyProvisioned, noFiles)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil) + shouldSkip := reader.createWalk(noFiles)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil) So(shouldSkip, ShouldEqual, filepath.SkipDir) }) Convey("should keep walking if file is not .json", func() { - shouldSkip := reader.createWalk(reader, 0, emptyProvisioned, noFiles)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil) + shouldSkip := reader.createWalk(noFiles)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil) So(shouldSkip, ShouldBeNil) }) }) @@ -260,6 +259,7 @@ func (repo *fakeDashboardRepo) GetProvisionedDashboardData(name string) ([]*mode func (repo *fakeDashboardRepo) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { repo.inserted = append(repo.inserted, dto) + repo.provisioned = append(repo.provisioned, provisioning) return dto.Dashboard, nil } diff --git a/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard1.json b/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard1.json index 5b6765a4ed6..febb98be0e8 100644 --- a/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard1.json +++ b/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard1.json @@ -1,5 +1,5 @@ { - "title": "Grafana", + "title": "Grafana1", "tags": [], "style": "dark", "timezone": "browser", @@ -170,4 +170,3 @@ }, "version": 5 } - \ No newline at end of file diff --git a/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard2.json b/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard2.json index 5b6765a4ed6..9291f16d9e7 100644 --- a/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard2.json +++ b/pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard2.json @@ -1,5 +1,5 @@ { - "title": "Grafana", + "title": "Grafana2", "tags": [], "style": "dark", "timezone": "browser", @@ -170,4 +170,3 @@ }, "version": 5 } - \ No newline at end of file From 41a6e7c2c86be7810032e40fe6816cf2c945493e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 9 Feb 2018 12:39:03 +0100 Subject: [PATCH 012/110] docs: updated docs landing page --- docs/sources/index.md | 134 ++++++++++++++++++++++++++++++------------ 1 file changed, 97 insertions(+), 37 deletions(-) diff --git a/docs/sources/index.md b/docs/sources/index.md index c1072db47a5..2a047b5257d 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -1,6 +1,6 @@ +++ -title = "Docs Home" -description = "Install guide for Grafana" +title = "Grafana documentation" +description = "Guides, Installation & Feature Documentation" keywords = ["grafana", "installation", "documentation"] type = "docs" aliases = ["v1.1", "guides/reference/admin"] @@ -8,42 +8,102 @@ aliases = ["v1.1", "guides/reference/admin"] # Welcome to the Grafana Documentation -Grafana is an open source metric analytics & visualization suite. It is most commonly used for -visualizing time series data for infrastructure and application analytics but many use it in -other domains including industrial sensors, home automation, weather, and process control. +Grafana is an open source metric analytics & visualization platform. It is most commonly used for visualizing time series data for infrastructure and application analytics but many use it in other domains including industrial sensors, home automation, weather, and process control. -## Installing Grafana -- [Installing on Debian / Ubuntu](installation/debian) -- [Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)](installation/rpm) -- [Installing on Mac OS X](installation/mac) -- [Installing on Windows](installation/windows) -- [Installing on Docker](installation/docker) -- [Installing using Provisioning (Chef, Puppet, Salt, Ansible, etc)](administration/provisioning#configuration-management-tools) -- [Nightly Builds](https://grafana.com/grafana/download) +

Installing Grafana

+ -For other platforms Read the [build from source]({{< relref "project/building_from_source.md" >}}) -instructions for more information. +

Guides

-## Configuring Grafana + -The back-end web server has a number of configuration options. Go the -[Configuration]({{< relref "installation/configuration.md" >}}) page for details on all -those options. - - -## Getting Started - -- [Getting Started]({{< relref "guides/getting_started.md" >}}) -- [Basic Concepts]({{< relref "guides/basic_concepts.md" >}}) -- [Screencasts]({{< relref "tutorials/screencasts.md" >}}) - -## Data Source Guides - -- [Graphite]({{< relref "features/datasources/graphite.md" >}}) -- [Elasticsearch]({{< relref "features/datasources/elasticsearch.md" >}}) -- [InfluxDB]({{< relref "features/datasources/influxdb.md" >}}) -- [Prometheus]({{< relref "features/datasources/prometheus.md" >}}) -- [OpenTSDB]({{< relref "features/datasources/opentsdb.md" >}}) -- [MySQL]({{< relref "features/datasources/mysql.md" >}}) -- [Postgres]({{< relref "features/datasources/postgres.md" >}}) -- [Cloudwatch]({{< relref "features/datasources/cloudwatch.md" >}}) +

Data Source Guides

+ From e672604835cbf0edca56d80ed3c4eecdccdf79fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 9 Feb 2018 12:53:07 +0100 Subject: [PATCH 013/110] docs: minor docs update --- docs/sources/index.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/sources/index.md b/docs/sources/index.md index 2a047b5257d..3c59b9baba0 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -6,9 +6,7 @@ type = "docs" aliases = ["v1.1", "guides/reference/admin"] +++ -# Welcome to the Grafana Documentation - -Grafana is an open source metric analytics & visualization platform. It is most commonly used for visualizing time series data for infrastructure and application analytics but many use it in other domains including industrial sensors, home automation, weather, and process control. +# Grafana Documentation

Installing Grafana