Tool to help you manage your Grafana dashboards using Git.

git.go 5.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. package git
  2. import (
  3. "io/ioutil"
  4. "os"
  5. "strings"
  6. "config"
  7. "github.com/sirupsen/logrus"
  8. "golang.org/x/crypto/ssh"
  9. gogit "gopkg.in/src-d/go-git.v4"
  10. gitssh "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh"
  11. )
  12. // Sync synchronises a Git repository using a given configuration. "synchronises"
  13. // means that, if the repo from the configuration isn't already cloned in the
  14. // directory specified in the configuration, it will clone the repository,
  15. // else it will simply pull it in order to be up to date with the remote.
  16. // Returns the go-git representation of the repository.
  17. // Returns an error if there was an issue loading the SSH private key, checking
  18. // whether the clone path already exists, or synchronising the repo with the
  19. // remote.
  20. func Sync(cfg config.GitSettings) (r *gogit.Repository, err error) {
  21. // Generate an authentication structure instance from the user and private
  22. // key
  23. auth, err := getAuth(cfg.User, cfg.PrivateKeyPath)
  24. if err != nil {
  25. return
  26. }
  27. // Check whether the clone path already exists
  28. exists, err := dirExists(cfg.ClonePath)
  29. if err != nil {
  30. return
  31. }
  32. logrus.WithFields(logrus.Fields{
  33. "repo": cfg.User + "@" + cfg.URL,
  34. "clone_path": cfg.ClonePath,
  35. "pull": exists,
  36. }).Info("Synchronising the Git repository with the remote")
  37. // If the clone path already exists, pull from the remote, else clone it.
  38. if exists {
  39. r, err = pull(cfg.ClonePath, auth)
  40. } else {
  41. r, err = clone(cfg.URL, cfg.ClonePath, auth)
  42. }
  43. return
  44. }
  45. // getAuth returns the authentication structure instance needed to authenticate
  46. // on the remote, using a given user and private key path.
  47. // Returns an error if there was an issue reading the private key file or
  48. // parsing it.
  49. func getAuth(user string, privateKeyPath string) (*gitssh.PublicKeys, error) {
  50. privateKey, err := ioutil.ReadFile(privateKeyPath)
  51. if err != nil {
  52. return nil, err
  53. }
  54. signer, err := ssh.ParsePrivateKey(privateKey)
  55. if err != nil {
  56. return nil, err
  57. }
  58. return &gitssh.PublicKeys{User: user, Signer: signer}, nil
  59. }
  60. // clone clones a Git repository into a given path, using a given auth.
  61. // Returns the go-git representation of the Git repository.
  62. // Returns an error if there was an issue cloning the repository.
  63. func clone(repo string, clonePath string, auth *gitssh.PublicKeys) (*gogit.Repository, error) {
  64. return gogit.PlainClone(clonePath, false, &gogit.CloneOptions{
  65. URL: repo,
  66. Auth: auth,
  67. })
  68. }
  69. // pull opens the repository located at a given path, and pulls it from the
  70. // remote using a given auth, in order to be up to date with the remote.
  71. // Returns with the go-git representation of the repository.
  72. // Returns an error if there was an issue opening the repo, getting its work
  73. // tree or pulling from the remote. In the latter case, if the error is "already
  74. // up to date" or "non-fast-forward update", doesn't return any error.
  75. func pull(clonePath string, auth *gitssh.PublicKeys) (*gogit.Repository, error) {
  76. // Open the repository
  77. r, err := gogit.PlainOpen(clonePath)
  78. if err != nil {
  79. return nil, err
  80. }
  81. // Get its worktree
  82. w, err := r.Worktree()
  83. if err != nil {
  84. return nil, err
  85. }
  86. // Pull from remote
  87. err = w.Pull(&gogit.PullOptions{
  88. RemoteName: "origin",
  89. Auth: auth,
  90. })
  91. // Don't return with an error for "already up to date" or "non-fast-forward
  92. // update"
  93. if err != nil {
  94. if err == gogit.NoErrAlreadyUpToDate {
  95. logrus.WithFields(logrus.Fields{
  96. "clone_path": clonePath,
  97. "error": err,
  98. }).Info("Caught specific non-error")
  99. return r, nil
  100. }
  101. // go-git doesn't have an error variable for "non-fast-forward update",
  102. // so this is the only way to detect it
  103. if strings.HasPrefix(err.Error(), "non-fast-forward update") {
  104. logrus.WithFields(logrus.Fields{
  105. "clone_path": clonePath,
  106. "error": err,
  107. }).Info("Caught specific non-error")
  108. return r, nil
  109. }
  110. }
  111. return r, err
  112. }
  113. // dirExists is a snippet checking if a directory exists on the disk.
  114. // Returns with a boolean set to true if the directory exists, false if not.
  115. // Returns with an error if there was an issue checking the directory's
  116. // existence.
  117. func dirExists(path string) (bool, error) {
  118. _, err := os.Stat(path)
  119. if os.IsNotExist(err) {
  120. return false, nil
  121. }
  122. return true, err
  123. }
  124. // Push uses a given repository and configuration to push the local history of
  125. // the said repository to the remote, using an authentication structure instance
  126. // created from the configuration to authenticate on the remote.
  127. // Returns with an error if there was an issue creating the authentication
  128. // structure instance or pushing to the remote. In the latter case, if the error
  129. // is "already up to date" or "non-fast-forward update", doesn't return any error.
  130. func Push(r *gogit.Repository, cfg config.GitSettings) error {
  131. // Get the authentication structure instance
  132. auth, err := getAuth(cfg.User, cfg.PrivateKeyPath)
  133. if err != nil {
  134. return err
  135. }
  136. logrus.WithFields(logrus.Fields{
  137. "repo": cfg.User + "@" + cfg.URL,
  138. "clone_path": cfg.ClonePath,
  139. }).Info("Pushing to the remote")
  140. // Push to remote
  141. err = r.Push(&gogit.PushOptions{
  142. Auth: auth,
  143. })
  144. // Don't return with an error for "already up to date" or "non-fast-forward
  145. // update"
  146. if err != nil {
  147. if err == gogit.NoErrAlreadyUpToDate {
  148. logrus.WithFields(logrus.Fields{
  149. "repo": cfg.User + "@" + cfg.URL,
  150. "clone_path": cfg.ClonePath,
  151. "error": err,
  152. }).Info("Caught specific non-error")
  153. return nil
  154. }
  155. // go-git doesn't have an error variable for "non-fast-forward update", so
  156. // this is the only way to detect it
  157. if strings.HasPrefix(err.Error(), "non-fast-forward update") {
  158. logrus.WithFields(logrus.Fields{
  159. "repo": cfg.User + "@" + cfg.URL,
  160. "clone_path": cfg.ClonePath,
  161. "error": err,
  162. }).Info("Caught specific non-error")
  163. return nil
  164. }
  165. }
  166. return err
  167. }