file_reader.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  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. createWalk func(filesOnDisk map[string]os.FileInfo) filepath.WalkFunc
  26. }
  27. func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
  28. var path string
  29. path, ok := cfg.Options["path"].(string)
  30. if !ok {
  31. path, ok = cfg.Options["folder"].(string)
  32. if !ok {
  33. return nil, fmt.Errorf("Failed to load dashboards. path param is not a string")
  34. }
  35. log.Warn("[Deprecated] The folder property is deprecated. Please use path instead.")
  36. }
  37. if _, err := os.Stat(path); os.IsNotExist(err) {
  38. log.Error("Cannot read directory", "error", err)
  39. }
  40. return &fileReader{
  41. Cfg: cfg,
  42. Path: path,
  43. log: log,
  44. dashboardRepo: dashboards.GetRepository(),
  45. createWalk: createWalkFn,
  46. }, nil
  47. }
  48. func (fr *fileReader) ReadAndListen(ctx context.Context) error {
  49. if err := fr.startWalkingDisk(); err != nil {
  50. fr.log.Error("failed to search for dashboards", "error", err)
  51. }
  52. ticker := time.NewTicker(checkDiskForChangesInterval)
  53. running := false
  54. for {
  55. select {
  56. case <-ticker.C:
  57. if !running { // avoid walking the filesystem in parallel. in-case fs is very slow.
  58. running = true
  59. go func() {
  60. if err := fr.startWalkingDisk(); err != nil {
  61. fr.log.Error("failed to search for dashboards", "error", err)
  62. }
  63. running = false
  64. }()
  65. }
  66. case <-ctx.Done():
  67. return nil
  68. }
  69. }
  70. }
  71. func (fr *fileReader) startWalkingDisk() error {
  72. if _, err := os.Stat(fr.Path); err != nil {
  73. if os.IsNotExist(err) {
  74. return err
  75. }
  76. }
  77. folderId, err := getOrCreateFolderId(fr.Cfg, fr.dashboardRepo)
  78. if err != nil && err != ErrFolderNameMissing {
  79. return err
  80. }
  81. provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardRepo, fr.Cfg.Name)
  82. if err != nil {
  83. return err
  84. }
  85. filesFoundOnDisk := map[string]os.FileInfo{}
  86. err = filepath.Walk(fr.Path, fr.createWalk(filesFoundOnDisk))
  87. if err != nil {
  88. return err
  89. }
  90. // find dashboards to delete since json file is missing
  91. var dashboardToDelete []int64
  92. for path, provisioningData := range provisionedDashboardRefs {
  93. _, existsInDatabase := filesFoundOnDisk[path]
  94. if !existsInDatabase {
  95. dashboardToDelete = append(dashboardToDelete, provisioningData.DashboardId)
  96. }
  97. }
  98. // delete dashboard that are missing json file
  99. for _, dashboardId := range dashboardToDelete {
  100. fr.log.Debug("deleting provisioned dashboard. missing on disk", "id", dashboardId)
  101. cmd := &models.DeleteDashboardCommand{OrgId: fr.Cfg.OrgId, Id: dashboardId}
  102. err := bus.Dispatch(cmd)
  103. if err != nil {
  104. return err
  105. }
  106. }
  107. // insert/update dashboards based on json files
  108. for path, fileInfo := range filesFoundOnDisk {
  109. err = fr.saveDashboard(path, folderId, fileInfo, provisionedDashboardRefs)
  110. if err != nil {
  111. fr.log.Error("Failed to save dashboard", "error", err)
  112. return err
  113. }
  114. }
  115. return nil
  116. }
  117. func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.FileInfo, provisionedDashboardRefs map[string]*models.DashboardProvisioning) error {
  118. resolvedFileInfo, err := resolveSymlink(fileInfo, path)
  119. if err != nil {
  120. return err
  121. }
  122. provisionedData, allReadyProvisioned := provisionedDashboardRefs[path]
  123. if allReadyProvisioned && provisionedData.Updated.Unix() == resolvedFileInfo.ModTime().Unix() {
  124. return nil // dashboard is already in sync with the database
  125. }
  126. dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId)
  127. if err != nil {
  128. fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
  129. return nil
  130. }
  131. if allReadyProvisioned {
  132. dash.Dashboard.SetId(provisionedData.DashboardId)
  133. }
  134. fr.log.Debug("saving new dashboard", "file", path)
  135. dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime()}
  136. _, err = fr.dashboardRepo.SaveProvisionedDashboard(dash, dp)
  137. return err
  138. }
  139. func getProvisionedDashboardByPath(repo dashboards.Repository, name string) (map[string]*models.DashboardProvisioning, error) {
  140. arr, err := repo.GetProvisionedDashboardData(name)
  141. if err != nil {
  142. return nil, err
  143. }
  144. byPath := map[string]*models.DashboardProvisioning{}
  145. for _, pd := range arr {
  146. byPath[pd.ExternalId] = pd
  147. }
  148. return byPath, nil
  149. }
  150. func getOrCreateFolderId(cfg *DashboardsAsConfig, repo dashboards.Repository) (int64, error) {
  151. if cfg.Folder == "" {
  152. return 0, ErrFolderNameMissing
  153. }
  154. cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(cfg.Folder), OrgId: cfg.OrgId}
  155. err := bus.Dispatch(cmd)
  156. if err != nil && err != models.ErrDashboardNotFound {
  157. return 0, err
  158. }
  159. // dashboard folder not found. create one.
  160. if err == models.ErrDashboardNotFound {
  161. dash := &dashboards.SaveDashboardDTO{}
  162. dash.Dashboard = models.NewDashboard(cfg.Folder)
  163. dash.Dashboard.IsFolder = true
  164. dash.Overwrite = true
  165. dash.OrgId = cfg.OrgId
  166. dbDash, err := repo.SaveDashboard(dash)
  167. if err != nil {
  168. return 0, err
  169. }
  170. return dbDash.Id, nil
  171. }
  172. if !cmd.Result.IsFolder {
  173. return 0, fmt.Errorf("got invalid response. expected folder, found dashboard")
  174. }
  175. return cmd.Result.Id, nil
  176. }
  177. func resolveSymlink(fileinfo os.FileInfo, path string) (os.FileInfo, error) {
  178. checkFilepath, err := filepath.EvalSymlinks(path)
  179. if path != checkFilepath {
  180. path = checkFilepath
  181. fi, err := os.Lstat(checkFilepath)
  182. if err != nil {
  183. return nil, err
  184. }
  185. return fi, nil
  186. }
  187. return fileinfo, err
  188. }
  189. func createWalkFn(filesOnDisk map[string]os.FileInfo) filepath.WalkFunc {
  190. return func(path string, fileInfo os.FileInfo, err error) error {
  191. if err != nil {
  192. return err
  193. }
  194. isValid, err := validateWalkablePath(fileInfo)
  195. if !isValid {
  196. return err
  197. }
  198. filesOnDisk[path] = fileInfo
  199. return nil
  200. }
  201. }
  202. func validateWalkablePath(fileInfo os.FileInfo) (bool, error) {
  203. if fileInfo.IsDir() {
  204. if strings.HasPrefix(fileInfo.Name(), ".") {
  205. return false, filepath.SkipDir
  206. }
  207. return false, nil
  208. }
  209. if !strings.HasSuffix(fileInfo.Name(), ".json") {
  210. return false, nil
  211. }
  212. return true, nil
  213. }
  214. func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboards.SaveDashboardDTO, error) {
  215. reader, err := os.Open(path)
  216. if err != nil {
  217. return nil, err
  218. }
  219. defer reader.Close()
  220. data, err := simplejson.NewFromReader(reader)
  221. if err != nil {
  222. return nil, err
  223. }
  224. dash, err := createDashboardJson(data, lastModified, fr.Cfg, folderId)
  225. if err != nil {
  226. return nil, err
  227. }
  228. return dash, nil
  229. }