Tool to help you manage your Grafana dashboards using Git.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. package main
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "io/ioutil"
  6. "os"
  7. "strings"
  8. "config"
  9. "git"
  10. "grafana"
  11. "github.com/sirupsen/logrus"
  12. gogit "gopkg.in/src-d/go-git.v4"
  13. )
  14. // diffVersion represents a dashboard version diff.
  15. type diffVersion struct {
  16. oldVersion int
  17. newVersion int
  18. }
  19. // PullGrafanaAndCommit pulls all the dashboards from Grafana except the ones
  20. // which name starts with "test", then commits each of them to Git except for
  21. // those that have a newer or equal version number already versionned in the
  22. // repo.
  23. func PullGrafanaAndCommit(client *grafana.Client, cfg *config.Config) error {
  24. // Clone or pull the repo
  25. repo, err := git.Sync(cfg.Git)
  26. if err != nil {
  27. return err
  28. }
  29. w, err := repo.Worktree()
  30. if err != nil {
  31. return err
  32. }
  33. // Get URIs for all known dashboards
  34. logrus.Info("Getting dashboard URIs")
  35. uris, err := client.GetDashboardsURIs()
  36. if err != nil {
  37. return err
  38. }
  39. dv := make(map[string]diffVersion)
  40. // Load versions
  41. logrus.Info("Getting local dashboard versions")
  42. dbVersions, err := getDashboardsVersions(cfg.Git.ClonePath)
  43. if err != nil {
  44. return err
  45. }
  46. // Iterate over the dashboards URIs
  47. for _, uri := range uris {
  48. logrus.WithFields(logrus.Fields{
  49. "uri": uri,
  50. }).Info("Retrieving dashboard")
  51. // Retrieve the dashboard JSON
  52. dashboard, err := client.GetDashboard(uri)
  53. if err != nil {
  54. return err
  55. }
  56. if len(cfg.Grafana.IgnorePrefix) > 0 {
  57. lowerCasedName := strings.ToLower(dashboard.Name)
  58. if strings.HasPrefix(lowerCasedName, cfg.Grafana.IgnorePrefix) {
  59. logrus.WithFields(logrus.Fields{
  60. "uri": uri,
  61. "name": dashboard.Name,
  62. "prefix": cfg.Grafana.IgnorePrefix,
  63. }).Info("Dashboard name starts with specified prefix, skipping")
  64. continue
  65. }
  66. }
  67. // Check if there's a version for this dashboard in the data loaded from
  68. // the "versions.json" file. If there's a version and it's older (lower
  69. // version number) than the version we just retrieved from the Grafana
  70. // API, or if there's no known version (ok will be false), write the
  71. // changes in the repo and add the modified file to the git index.
  72. version, ok := dbVersions[dashboard.Slug]
  73. if !ok || dashboard.Version > version {
  74. logrus.WithFields(logrus.Fields{
  75. "uri": uri,
  76. "name": dashboard.Name,
  77. "local_version": version,
  78. "new_version": dashboard.Version,
  79. }).Info("Grafana has a newer version, updating")
  80. if err = addDashboardChangesToRepo(
  81. dashboard, cfg.Git.ClonePath, w,
  82. ); err != nil {
  83. return err
  84. }
  85. // We don't need to check for the value of ok because if ok is false
  86. // version will be initialised to the 0-value of the int type, which
  87. // is 0, so the previous version number will be considered to be 0,
  88. // which is the behaviour we want.
  89. dv[dashboard.Slug] = diffVersion{
  90. oldVersion: version,
  91. newVersion: dashboard.Version,
  92. }
  93. }
  94. }
  95. status, err := w.Status()
  96. if err != nil {
  97. return err
  98. }
  99. // Check if there's uncommited changes, and if that's the case, commit them.
  100. if !status.IsClean() {
  101. logrus.Info("Comitting changes")
  102. if err = commitNewVersions(dbVersions, dv, w, cfg); err != nil {
  103. return err
  104. }
  105. }
  106. // Push the changes (we don't do it in the if clause above in case there are
  107. // pending commits in the local repo that haven't been pushed yet).
  108. if err = git.Push(repo, cfg.Git); err != nil {
  109. return err
  110. }
  111. return nil
  112. }
  113. // addDashboardChangesToRepo writes a dashboard content in a file, then adds the
  114. // file to the git index so it can be comitted afterwards.
  115. // Returns an error if there was an issue with either of the steps.
  116. func addDashboardChangesToRepo(
  117. dashboard *grafana.Dashboard, clonePath string, worktree *gogit.Worktree,
  118. ) error {
  119. slugExt := dashboard.Slug + ".json"
  120. if err := rewriteFile(clonePath+"/"+slugExt, dashboard.RawJSON); err != nil {
  121. return err
  122. }
  123. if _, err := worktree.Add(slugExt); err != nil {
  124. return err
  125. }
  126. return nil
  127. }
  128. // rewriteFile removes a given file and re-creates it with a new content. The
  129. // content is provided as JSON, and is then indented before being written down.
  130. // We need the whole "remove then recreate" thing because, if the file already
  131. // exists, ioutil.WriteFile will append the content to it. However, we want to
  132. // replace the oldest version with another (so git can diff it), so we re-create
  133. // the file with the changed content.
  134. // Returns an error if there was an issue when removing or writing the file, or
  135. // indenting the JSON content.
  136. func rewriteFile(filename string, content []byte) error {
  137. if err := os.Remove(filename); err != nil {
  138. pe, ok := err.(*os.PathError)
  139. if !ok || pe.Err.Error() != "no such file or directory" {
  140. return err
  141. }
  142. }
  143. indentedContent, err := indent(content)
  144. if err != nil {
  145. return err
  146. }
  147. return ioutil.WriteFile(filename, indentedContent, 0644)
  148. }
  149. // indent indents a given JSON content with tabs.
  150. // We need to indent the content as the Grafana API returns a one-lined JSON
  151. // string, which isn't great to work with.
  152. // Returns an error if there was an issue with the process.
  153. func indent(srcJSON []byte) (indentedJSON []byte, err error) {
  154. buf := bytes.NewBuffer(nil)
  155. if err = json.Indent(buf, srcJSON, "", "\t"); err != nil {
  156. return
  157. }
  158. indentedJSON, err = ioutil.ReadAll(buf)
  159. return
  160. }