Tool to help you manage your Grafana dashboards using Git.

webhook.go 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. package main
  2. import (
  3. "encoding/json"
  4. "io/ioutil"
  5. "strings"
  6. "config"
  7. "git"
  8. puller "puller"
  9. "github.com/sirupsen/logrus"
  10. "gopkg.in/go-playground/webhooks.v3"
  11. "gopkg.in/go-playground/webhooks.v3/gitlab"
  12. )
  13. // SetupWebhook creates and exposes a GitLab webhook using a given configuration.
  14. // Returns an error if the webhook couldn't be set up.
  15. func SetupWebhook(cfg *config.Config) error {
  16. hook := gitlab.New(&gitlab.Config{
  17. Secret: cfg.Webhook.Secret,
  18. })
  19. hook.RegisterEvents(HandlePush, gitlab.PushEvents)
  20. return webhooks.Run(
  21. hook,
  22. cfg.Webhook.Interface+":"+cfg.Webhook.Port,
  23. cfg.Webhook.Path,
  24. )
  25. }
  26. // HandlePush is called each time a push event is sent by GitLab on the webhook.
  27. func HandlePush(payload interface{}, header webhooks.Header) {
  28. var err error
  29. // Process the payload using the right structure
  30. pl := payload.(gitlab.PushEventPayload)
  31. // Only push changes made on master to Grafana
  32. if pl.Ref != "refs/heads/master" {
  33. return
  34. }
  35. // Clone or pull the repository
  36. if _, err = git.Sync(cfg.Git); err != nil {
  37. logrus.WithFields(logrus.Fields{
  38. "error": err,
  39. "repo": cfg.Git.User + "@" + cfg.Git.URL,
  40. "clone_path": cfg.Git.ClonePath,
  41. }).Error("Failed to synchronise the Git repository with the remote")
  42. return
  43. }
  44. // Files to push are stored in a map before being pushed to the Grafana API.
  45. // We don't push them in the loop iterating over commits because, in the
  46. // case a file is successively updated by two commits pushed at the same
  47. // time, it would push the same file several time, which isn't an optimised
  48. // behaviour.
  49. filesToPush := make(map[string]bool)
  50. // Iterate over the commits descriptions from the payload
  51. for _, commit := range pl.Commits {
  52. // We don't want to process commits made by the puller
  53. if commit.Author.Email == cfg.Git.CommitsAuthor.Email {
  54. logrus.WithFields(logrus.Fields{
  55. "hash": commit.ID,
  56. "author_email": commit.Author.Email,
  57. "manager_email": cfg.Git.CommitsAuthor.Email,
  58. }).Info("Commit was made by the manager, skipping")
  59. continue
  60. }
  61. // Push all added files, except the ones describing a dashboard which
  62. // name starts with a the prefix specified in the configuration file.
  63. for _, addedFile := range commit.Added {
  64. ignored, err := isIgnored(addedFile)
  65. if err != nil {
  66. logrus.WithFields(logrus.Fields{
  67. "error": err,
  68. "filename": addedFile,
  69. }).Error("Failed to check if file is to be ignored")
  70. continue
  71. }
  72. if !ignored {
  73. logrus.WithFields(logrus.Fields{
  74. "filename": addedFile,
  75. }).Info("Setting file as file to push to Grafana")
  76. filesToPush[addedFile] = true
  77. }
  78. }
  79. // Push all modified files, except the ones describing a dashboard which
  80. // name starts with a the prefix specified in the configuration file.
  81. for _, modifiedFile := range commit.Modified {
  82. ignored, err := isIgnored(modifiedFile)
  83. if err != nil {
  84. logrus.WithFields(logrus.Fields{
  85. "error": err,
  86. "filename": modifiedFile,
  87. }).Error("Failed to check if file is to be ignored")
  88. continue
  89. }
  90. if !ignored {
  91. logrus.WithFields(logrus.Fields{
  92. "filename": modifiedFile,
  93. }).Info("Setting file as file to push to Grafana")
  94. filesToPush[modifiedFile] = true
  95. }
  96. }
  97. // TODO: Remove a dashboard when its file gets deleted?
  98. }
  99. // Push all files to the Grafana API
  100. for fileToPush := range filesToPush {
  101. if err = pushFile(fileToPush); err != nil {
  102. logrus.WithFields(logrus.Fields{
  103. "error": err,
  104. "filename": fileToPush,
  105. }).Error("Failed push the file to Grafana")
  106. continue
  107. }
  108. }
  109. // Grafana will auto-update the version number after we pushed the new
  110. // dashboards, so we use the puller mechanic to pull the updated numbers and
  111. // commit them in the git repo.
  112. if err = puller.PullGrafanaAndCommit(grafanaClient, cfg); err != nil {
  113. logrus.WithFields(logrus.Fields{
  114. "error": err,
  115. "repo": cfg.Git.User + "@" + cfg.Git.URL,
  116. "clone_path": cfg.Git.ClonePath,
  117. }).Error("Call to puller returned an error")
  118. }
  119. }
  120. // pushFile pushes the content of a given file to the Grafana API in order to
  121. // create or update a dashboard.
  122. // Returns an error if there was an issue reading the file or sending its content
  123. // to the Grafana instance.
  124. func pushFile(filename string) error {
  125. filePath := cfg.Git.ClonePath + "/" + filename
  126. fileContent, err := ioutil.ReadFile(filePath)
  127. if err != nil {
  128. return err
  129. }
  130. // Remove the .json part
  131. slug := strings.Split(filename, ".json")[0]
  132. return grafanaClient.CreateOrUpdateDashboard(slug, fileContent)
  133. }
  134. // isIgnored checks whether the file must be ignored, by checking if there's an
  135. // prefix for ignored files set in the configuration file, and if the dashboard
  136. // described in the file has a name that starts with this prefix. Returns an
  137. // error if there was an issue reading or decoding the file.
  138. // TODO: Optimise this part of the workflow, as all files get open twice (here
  139. // and in pushFile)
  140. func isIgnored(filename string) (bool, error) {
  141. // Always ignore versions.json
  142. if strings.HasSuffix(filename, "versions.json") {
  143. return true, nil
  144. }
  145. // If there's no prefix set, no file is ignored
  146. if len(cfg.Grafana.IgnorePrefix) == 0 {
  147. return false, nil
  148. }
  149. // Read the file's content
  150. fileContent, err := ioutil.ReadFile(filename)
  151. if err != nil {
  152. return false, err
  153. }
  154. // Parse the file's content to find the dashboard's name
  155. var dashboardName struct {
  156. Name string `json:"title"`
  157. }
  158. if err = json.Unmarshal(fileContent, &dashboardName); err != nil {
  159. return false, err
  160. }
  161. // Compare the lower case dashboar name to the prefix (which has already
  162. // been lower cased when loading the configuration file)
  163. lowerCaseName := strings.ToLower(dashboardName.Name)
  164. if strings.HasPrefix(lowerCaseName, cfg.Grafana.IgnorePrefix) {
  165. return true, nil
  166. }
  167. return false, nil
  168. }