file_reader.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. package dashboards
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "time"
  10. "github.com/grafana/grafana/pkg/services/dashboards"
  11. "github.com/grafana/grafana/pkg/bus"
  12. "github.com/grafana/grafana/pkg/components/simplejson"
  13. "github.com/grafana/grafana/pkg/log"
  14. "github.com/grafana/grafana/pkg/models"
  15. )
  16. var (
  17. checkDiskForChangesInterval time.Duration = time.Second * 3
  18. ErrFolderNameMissing error = errors.New("Folder name missing")
  19. )
  20. type fileReader struct {
  21. Cfg *DashboardsAsConfig
  22. Path string
  23. log log.Logger
  24. dashboardRepo dashboards.Repository
  25. cache *dashboardCache
  26. createWalk func(fr *fileReader, folderId int64) filepath.WalkFunc
  27. }
  28. func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
  29. var path string
  30. path, ok := cfg.Options["path"].(string)
  31. if !ok {
  32. path, ok = cfg.Options["folder"].(string)
  33. if !ok {
  34. return nil, fmt.Errorf("Failed to load dashboards. path param is not a string")
  35. }
  36. log.Warn("[Deprecated] The folder property is deprecated. Please use path instead.")
  37. }
  38. if _, err := os.Stat(path); os.IsNotExist(err) {
  39. log.Error("Cannot read directory", "error", err)
  40. }
  41. return &fileReader{
  42. Cfg: cfg,
  43. Path: path,
  44. log: log,
  45. dashboardRepo: dashboards.GetRepository(),
  46. cache: NewDashboardCache(),
  47. createWalk: createWalkFn,
  48. }, nil
  49. }
  50. func (fr *fileReader) ReadAndListen(ctx context.Context) error {
  51. if err := fr.startWalkingDisk(); err != nil {
  52. fr.log.Error("failed to search for dashboards", "error", err)
  53. }
  54. ticker := time.NewTicker(checkDiskForChangesInterval)
  55. running := false
  56. for {
  57. select {
  58. case <-ticker.C:
  59. if !running { // avoid walking the filesystem in parallel. in-case fs is very slow.
  60. running = true
  61. go func() {
  62. if err := fr.startWalkingDisk(); err != nil {
  63. fr.log.Error("failed to search for dashboards", "error", err)
  64. }
  65. running = false
  66. }()
  67. }
  68. case <-ctx.Done():
  69. return nil
  70. }
  71. }
  72. }
  73. func (fr *fileReader) startWalkingDisk() error {
  74. if _, err := os.Stat(fr.Path); err != nil {
  75. if os.IsNotExist(err) {
  76. return err
  77. }
  78. }
  79. folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardRepo)
  80. if err != nil && err != ErrFolderNameMissing {
  81. return err
  82. }
  83. return filepath.Walk(fr.Path, fr.createWalk(fr, folderId))
  84. }
  85. func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (int64, error) {
  86. if cfg.Folder == "" {
  87. return 0, ErrFolderNameMissing
  88. }
  89. cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(cfg.Folder), OrgId: cfg.OrgId}
  90. err := bus.Dispatch(cmd)
  91. if err != nil && err != models.ErrDashboardNotFound {
  92. return 0, err
  93. }
  94. // dashboard folder not found. create one.
  95. if err == models.ErrDashboardNotFound {
  96. dash := &dashboards.SaveDashboardDTO{}
  97. dash.Dashboard = models.NewDashboard(cfg.Folder)
  98. dash.Dashboard.IsFolder = true
  99. dash.Overwrite = true
  100. dash.OrgId = cfg.OrgId
  101. dbDash, err := repo.SaveDashboard(dash)
  102. if err != nil {
  103. return 0, err
  104. }
  105. return dbDash.Id, nil
  106. }
  107. if !cmd.Result.IsFolder {
  108. return 0, fmt.Errorf("got invalid response. expected folder, found dashboard")
  109. }
  110. return cmd.Result.Id, nil
  111. }
  112. func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) {
  113. checkFilepath, err := filepath.EvalSymlinks(path)
  114. if path != checkFilepath {
  115. path = checkFilepath
  116. fi, err := os.Lstat(checkFilepath)
  117. if err != nil {
  118. return nil, err
  119. }
  120. return fi, nil
  121. }
  122. return fileinfo, err
  123. }
  124. func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc {
  125. return func(path string, fileInfo os.FileInfo, err error) error {
  126. if err != nil {
  127. return err
  128. }
  129. isValid, err := validateWalkablePath(fileInfo)
  130. if !isValid {
  131. return err
  132. }
  133. resolvedFileInfo, err := resolveSymlink(fileInfo, path)
  134. if err != nil {
  135. return err
  136. }
  137. cachedDashboard, exist := fr.cache.getCache(path)
  138. if exist && cachedDashboard.UpdatedAt == resolvedFileInfo.ModTime() {
  139. return nil
  140. }
  141. dash, err := fr.readDashboardFromFile(path, folderId)
  142. if err != nil {
  143. fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
  144. return nil
  145. }
  146. if dash.Dashboard.Id != 0 {
  147. fr.log.Error("Cannot provision dashboard. Please remove the id property from the json file")
  148. return nil
  149. }
  150. cmd := &models.GetDashboardQuery{Slug: dash.Dashboard.Slug}
  151. err = bus.Dispatch(cmd)
  152. // if we don't have the dashboard in the db, save it!
  153. if err == models.ErrDashboardNotFound {
  154. fr.log.Debug("saving new dashboard", "file", path)
  155. err = saveDashboard(fr, path, dash, fileInfo.ModTime())
  156. return err
  157. }
  158. if err != nil {
  159. fr.log.Error("failed to query for dashboard", "slug", dash.Dashboard.Slug, "error", err)
  160. return nil
  161. }
  162. // break if db version is newer then fil version
  163. if cmd.Result.Updated.Unix() >= resolvedFileInfo.ModTime().Unix() {
  164. return nil
  165. }
  166. fr.log.Debug("loading dashboard from disk into database.", "file", path)
  167. err = saveDashboard(fr, path, dash, fileInfo.ModTime())
  168. return err
  169. }
  170. }
  171. func saveDashboard(fr *fileReader, path string, dash *dashboards.SaveDashboardDTO, modTime time.Time) error {
  172. d := &models.DashboardProvisioning{
  173. ExternalId: path,
  174. Name: fr.Cfg.Name,
  175. }
  176. _, err := fr.dashboardRepo.SaveProvisionedDashboard(dash, d)
  177. return err
  178. }
  179. func validateWalkablePath(fileInfo os.FileInfo) (bool, error) {
  180. if fileInfo.IsDir() {
  181. if strings.HasPrefix(fileInfo.Name(), ".") {
  182. return false, filepath.SkipDir
  183. }
  184. return false, nil
  185. }
  186. if !strings.HasSuffix(fileInfo.Name(), ".json") {
  187. return false, nil
  188. }
  189. return true, nil
  190. }
  191. func (fr *fileReader) readDashboardFromFile(path string, folderId int64) (*dashboards.SaveDashboardDTO, error) {
  192. reader, err := os.Open(path)
  193. if err != nil {
  194. return nil, err
  195. }
  196. defer reader.Close()
  197. data, err := simplejson.NewFromReader(reader)
  198. if err != nil {
  199. return nil, err
  200. }
  201. stat, err := os.Stat(path)
  202. if err != nil {
  203. return nil, err
  204. }
  205. dash, err := createDashboardJson(data, stat.ModTime(), fr.Cfg, folderId)
  206. if err != nil {
  207. return nil, err
  208. }
  209. fr.cache.addDashboardCache(path, dash)
  210. return dash, nil
  211. }