file_reader_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. package dashboards
  2. import (
  3. "github.com/grafana/grafana/pkg/util"
  4. "math/rand"
  5. "os"
  6. "path/filepath"
  7. "runtime"
  8. "testing"
  9. "time"
  10. "github.com/grafana/grafana/pkg/bus"
  11. "github.com/grafana/grafana/pkg/models"
  12. "github.com/grafana/grafana/pkg/services/dashboards"
  13. "github.com/grafana/grafana/pkg/infra/log"
  14. . "github.com/smartystreets/goconvey/convey"
  15. )
  16. var (
  17. defaultDashboards = "testdata/test-dashboards/folder-one"
  18. brokenDashboards = "testdata/test-dashboards/broken-dashboards"
  19. oneDashboard = "testdata/test-dashboards/one-dashboard"
  20. containingId = "testdata/test-dashboards/containing-id"
  21. unprovision = "testdata/test-dashboards/unprovision"
  22. fakeService *fakeDashboardProvisioningService
  23. )
  24. func TestCreatingNewDashboardFileReader(t *testing.T) {
  25. Convey("creating new dashboard file reader", t, func() {
  26. cfg := &DashboardsAsConfig{
  27. Name: "Default",
  28. Type: "file",
  29. OrgId: 1,
  30. Folder: "",
  31. Options: map[string]interface{}{},
  32. }
  33. Convey("using path parameter", func() {
  34. cfg.Options["path"] = defaultDashboards
  35. reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
  36. So(err, ShouldBeNil)
  37. So(reader.Path, ShouldNotEqual, "")
  38. })
  39. Convey("using folder as options", func() {
  40. cfg.Options["folder"] = defaultDashboards
  41. reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
  42. So(err, ShouldBeNil)
  43. So(reader.Path, ShouldNotEqual, "")
  44. })
  45. Convey("using full path", func() {
  46. fullPath := "/var/lib/grafana/dashboards"
  47. if runtime.GOOS == "windows" {
  48. fullPath = `c:\var\lib\grafana`
  49. }
  50. cfg.Options["folder"] = fullPath
  51. reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
  52. So(err, ShouldBeNil)
  53. So(reader.Path, ShouldEqual, fullPath)
  54. So(filepath.IsAbs(reader.Path), ShouldBeTrue)
  55. })
  56. Convey("using relative path", func() {
  57. cfg.Options["folder"] = defaultDashboards
  58. reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
  59. So(err, ShouldBeNil)
  60. resolvedPath := reader.resolvedPath()
  61. So(filepath.IsAbs(resolvedPath), ShouldBeTrue)
  62. })
  63. })
  64. }
  65. func TestDashboardFileReader(t *testing.T) {
  66. Convey("Dashboard file reader", t, func() {
  67. bus.ClearBusHandlers()
  68. origNewDashboardProvisioningService := dashboards.NewProvisioningService
  69. fakeService = mockDashboardProvisioningService()
  70. bus.AddHandler("test", mockGetDashboardQuery)
  71. logger := log.New("test.logger")
  72. Convey("Reading dashboards from disk", func() {
  73. cfg := &DashboardsAsConfig{
  74. Name: "Default",
  75. Type: "file",
  76. OrgId: 1,
  77. Folder: "",
  78. Options: map[string]interface{}{},
  79. }
  80. Convey("Can read default dashboard", func() {
  81. cfg.Options["path"] = defaultDashboards
  82. cfg.Folder = "Team A"
  83. reader, err := NewDashboardFileReader(cfg, logger)
  84. So(err, ShouldBeNil)
  85. err = reader.startWalkingDisk()
  86. So(err, ShouldBeNil)
  87. folders := 0
  88. dashboards := 0
  89. for _, i := range fakeService.inserted {
  90. if i.Dashboard.IsFolder {
  91. folders++
  92. } else {
  93. dashboards++
  94. }
  95. }
  96. So(folders, ShouldEqual, 1)
  97. So(dashboards, ShouldEqual, 2)
  98. })
  99. Convey("Can read default dashboard and replace old version in database", func() {
  100. cfg.Options["path"] = oneDashboard
  101. stat, _ := os.Stat(oneDashboard + "/dashboard1.json")
  102. fakeService.getDashboard = append(fakeService.getDashboard, &models.Dashboard{
  103. Updated: stat.ModTime().AddDate(0, 0, -1),
  104. Slug: "grafana",
  105. })
  106. reader, err := NewDashboardFileReader(cfg, logger)
  107. So(err, ShouldBeNil)
  108. err = reader.startWalkingDisk()
  109. So(err, ShouldBeNil)
  110. So(len(fakeService.inserted), ShouldEqual, 1)
  111. })
  112. Convey("Overrides id from dashboard.json files", func() {
  113. cfg.Options["path"] = containingId
  114. reader, err := NewDashboardFileReader(cfg, logger)
  115. So(err, ShouldBeNil)
  116. err = reader.startWalkingDisk()
  117. So(err, ShouldBeNil)
  118. So(len(fakeService.inserted), ShouldEqual, 1)
  119. })
  120. Convey("Invalid configuration should return error", func() {
  121. cfg := &DashboardsAsConfig{
  122. Name: "Default",
  123. Type: "file",
  124. OrgId: 1,
  125. Folder: "",
  126. }
  127. _, err := NewDashboardFileReader(cfg, logger)
  128. So(err, ShouldNotBeNil)
  129. })
  130. Convey("Broken dashboards should not cause error", func() {
  131. cfg.Options["path"] = brokenDashboards
  132. _, err := NewDashboardFileReader(cfg, logger)
  133. So(err, ShouldBeNil)
  134. })
  135. Convey("Two dashboard providers should be able to provisioned the same dashboard without uid", func() {
  136. cfg1 := &DashboardsAsConfig{Name: "1", Type: "file", OrgId: 1, Folder: "f1", Options: map[string]interface{}{"path": containingId}}
  137. cfg2 := &DashboardsAsConfig{Name: "2", Type: "file", OrgId: 1, Folder: "f2", Options: map[string]interface{}{"path": containingId}}
  138. reader1, err := NewDashboardFileReader(cfg1, logger)
  139. So(err, ShouldBeNil)
  140. err = reader1.startWalkingDisk()
  141. So(err, ShouldBeNil)
  142. reader2, err := NewDashboardFileReader(cfg2, logger)
  143. So(err, ShouldBeNil)
  144. err = reader2.startWalkingDisk()
  145. So(err, ShouldBeNil)
  146. var folderCount int
  147. var dashCount int
  148. for _, o := range fakeService.inserted {
  149. if o.Dashboard.IsFolder {
  150. folderCount++
  151. } else {
  152. dashCount++
  153. }
  154. }
  155. So(folderCount, ShouldEqual, 2)
  156. So(dashCount, ShouldEqual, 2)
  157. })
  158. })
  159. Convey("Should not create new folder if folder name is missing", func() {
  160. cfg := &DashboardsAsConfig{
  161. Name: "Default",
  162. Type: "file",
  163. OrgId: 1,
  164. Folder: "",
  165. Options: map[string]interface{}{
  166. "folder": defaultDashboards,
  167. },
  168. }
  169. _, err := getOrCreateFolderId(cfg, fakeService)
  170. So(err, ShouldEqual, ErrFolderNameMissing)
  171. })
  172. Convey("can get or Create dashboard folder", func() {
  173. cfg := &DashboardsAsConfig{
  174. Name: "Default",
  175. Type: "file",
  176. OrgId: 1,
  177. Folder: "TEAM A",
  178. Options: map[string]interface{}{
  179. "folder": defaultDashboards,
  180. },
  181. }
  182. folderId, err := getOrCreateFolderId(cfg, fakeService)
  183. So(err, ShouldBeNil)
  184. inserted := false
  185. for _, d := range fakeService.inserted {
  186. if d.Dashboard.IsFolder && d.Dashboard.Id == folderId {
  187. inserted = true
  188. }
  189. }
  190. So(len(fakeService.inserted), ShouldEqual, 1)
  191. So(inserted, ShouldBeTrue)
  192. })
  193. Convey("Walking the folder with dashboards", func() {
  194. noFiles := map[string]os.FileInfo{}
  195. Convey("should skip dirs that starts with .", func() {
  196. shouldSkip := createWalkFn(noFiles)("path", &FakeFileInfo{isDirectory: true, name: ".folder"}, nil)
  197. So(shouldSkip, ShouldEqual, filepath.SkipDir)
  198. })
  199. Convey("should keep walking if file is not .json", func() {
  200. shouldSkip := createWalkFn(noFiles)("path", &FakeFileInfo{isDirectory: true, name: "folder"}, nil)
  201. So(shouldSkip, ShouldBeNil)
  202. })
  203. })
  204. Convey("Given missing dashboard file", func() {
  205. cfg := &DashboardsAsConfig{
  206. Name: "Default",
  207. Type: "file",
  208. OrgId: 1,
  209. Options: map[string]interface{}{
  210. "folder": unprovision,
  211. },
  212. }
  213. fakeService.inserted = []*dashboards.SaveDashboardDTO{
  214. {Dashboard: &models.Dashboard{Id: 1}},
  215. {Dashboard: &models.Dashboard{Id: 2}},
  216. }
  217. absPath1, err := filepath.Abs(unprovision + "/dashboard1.json")
  218. So(err, ShouldBeNil)
  219. // This one does not exist on disc, simulating a deleted file
  220. absPath2, err := filepath.Abs(unprovision + "/dashboard2.json")
  221. So(err, ShouldBeNil)
  222. fakeService.provisioned = map[string][]*models.DashboardProvisioning{
  223. "Default": {
  224. {DashboardId: 1, Name: "Default", ExternalId: absPath1},
  225. {DashboardId: 2, Name: "Default", ExternalId: absPath2},
  226. },
  227. }
  228. Convey("Missing dashboard should be unprovisioned if DisableDeletion = true", func() {
  229. cfg.DisableDeletion = true
  230. reader, err := NewDashboardFileReader(cfg, logger)
  231. So(err, ShouldBeNil)
  232. err = reader.startWalkingDisk()
  233. So(err, ShouldBeNil)
  234. So(len(fakeService.provisioned["Default"]), ShouldEqual, 1)
  235. So(fakeService.provisioned["Default"][0].ExternalId, ShouldEqual, absPath1)
  236. })
  237. Convey("Missing dashboard should be deleted if DisableDeletion = false", func() {
  238. reader, err := NewDashboardFileReader(cfg, logger)
  239. So(err, ShouldBeNil)
  240. err = reader.startWalkingDisk()
  241. So(err, ShouldBeNil)
  242. So(len(fakeService.provisioned["Default"]), ShouldEqual, 1)
  243. So(fakeService.provisioned["Default"][0].ExternalId, ShouldEqual, absPath1)
  244. So(len(fakeService.inserted), ShouldEqual, 1)
  245. So(fakeService.inserted[0].Dashboard.Id, ShouldEqual, 1)
  246. })
  247. })
  248. Reset(func() {
  249. dashboards.NewProvisioningService = origNewDashboardProvisioningService
  250. })
  251. })
  252. }
  253. type FakeFileInfo struct {
  254. isDirectory bool
  255. name string
  256. }
  257. func (ffi *FakeFileInfo) IsDir() bool {
  258. return ffi.isDirectory
  259. }
  260. func (ffi FakeFileInfo) Size() int64 {
  261. return 1
  262. }
  263. func (ffi FakeFileInfo) Mode() os.FileMode {
  264. return 0777
  265. }
  266. func (ffi FakeFileInfo) Name() string {
  267. return ffi.name
  268. }
  269. func (ffi FakeFileInfo) ModTime() time.Time {
  270. return time.Time{}
  271. }
  272. func (ffi FakeFileInfo) Sys() interface{} {
  273. return nil
  274. }
  275. func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
  276. mock := fakeDashboardProvisioningService{
  277. provisioned: map[string][]*models.DashboardProvisioning{},
  278. }
  279. dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService {
  280. return &mock
  281. }
  282. return &mock
  283. }
  284. type fakeDashboardProvisioningService struct {
  285. inserted []*dashboards.SaveDashboardDTO
  286. provisioned map[string][]*models.DashboardProvisioning
  287. getDashboard []*models.Dashboard
  288. }
  289. func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
  290. if _, ok := s.provisioned[name]; !ok {
  291. s.provisioned[name] = []*models.DashboardProvisioning{}
  292. }
  293. return s.provisioned[name], nil
  294. }
  295. func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
  296. // Copy the structs as we need to change them but do not want to alter outside world.
  297. var copyProvisioning = &models.DashboardProvisioning{}
  298. *copyProvisioning = *provisioning
  299. var copyDto = &dashboards.SaveDashboardDTO{}
  300. *copyDto = *dto
  301. if copyDto.Dashboard.Id == 0 {
  302. copyDto.Dashboard.Id = rand.Int63n(1000000)
  303. } else {
  304. err := s.DeleteProvisionedDashboard(dto.Dashboard.Id, dto.Dashboard.OrgId)
  305. // Lets delete existing so we do not have duplicates
  306. if err != nil {
  307. return nil, err
  308. }
  309. }
  310. s.inserted = append(s.inserted, dto)
  311. if _, ok := s.provisioned[provisioning.Name]; !ok {
  312. s.provisioned[provisioning.Name] = []*models.DashboardProvisioning{}
  313. }
  314. for _, val := range s.provisioned[provisioning.Name] {
  315. if val.DashboardId == dto.Dashboard.Id && val.Name == provisioning.Name {
  316. // Do not insert duplicates
  317. return dto.Dashboard, nil
  318. }
  319. }
  320. copyProvisioning.DashboardId = copyDto.Dashboard.Id
  321. s.provisioned[provisioning.Name] = append(s.provisioned[provisioning.Name], copyProvisioning)
  322. return dto.Dashboard, nil
  323. }
  324. func (s *fakeDashboardProvisioningService) SaveFolderForProvisionedDashboards(dto *dashboards.SaveDashboardDTO) (*models.Dashboard, error) {
  325. s.inserted = append(s.inserted, dto)
  326. return dto.Dashboard, nil
  327. }
  328. func (s *fakeDashboardProvisioningService) UnprovisionDashboard(dashboardId int64) error {
  329. for key, val := range s.provisioned {
  330. for index, dashboard := range val {
  331. if dashboard.DashboardId == dashboardId {
  332. s.provisioned[key] = append(s.provisioned[key][:index], s.provisioned[key][index+1:]...)
  333. }
  334. }
  335. }
  336. return nil
  337. }
  338. func (s *fakeDashboardProvisioningService) DeleteProvisionedDashboard(dashboardId int64, orgId int64) error {
  339. err := s.UnprovisionDashboard(dashboardId)
  340. if err != nil {
  341. return err
  342. }
  343. for index, val := range s.inserted {
  344. if val.Dashboard.Id == dashboardId {
  345. s.inserted = append(s.inserted[:index], s.inserted[util.MinInt(index+1, len(s.inserted)):]...)
  346. }
  347. }
  348. return nil
  349. }
  350. func (s *fakeDashboardProvisioningService) GetProvisionedDashboardDataByDashboardId(dashboardId int64) (*models.DashboardProvisioning, error) {
  351. return nil, nil
  352. }
  353. func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error {
  354. for _, d := range fakeService.getDashboard {
  355. if d.Slug == cmd.Slug {
  356. cmd.Result = d
  357. return nil
  358. }
  359. }
  360. return models.ErrDashboardNotFound
  361. }