GitOps for k8s
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

operations.go 9.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. package git
  2. import (
  3. "bufio"
  4. "bytes"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "io/ioutil"
  9. "os/exec"
  10. "path/filepath"
  11. "strings"
  12. "context"
  13. "github.com/pkg/errors"
  14. )
  15. // If true, every git invocation will be echoed to stdout
  16. const trace = false
  17. func config(ctx context.Context, workingDir, user, email string) error {
  18. for k, v := range map[string]string{
  19. "user.name": user,
  20. "user.email": email,
  21. } {
  22. if err := execGitCmd(ctx, workingDir, nil, "config", k, v); err != nil {
  23. return errors.Wrap(err, "setting git config")
  24. }
  25. }
  26. return nil
  27. }
  28. func clone(ctx context.Context, workingDir, repoURL, repoBranch string) (path string, err error) {
  29. repoPath := filepath.Join(workingDir, "repo")
  30. args := []string{"clone"}
  31. if repoBranch != "" {
  32. args = append(args, "--branch", repoBranch)
  33. }
  34. args = append(args, repoURL, repoPath)
  35. if err := execGitCmd(ctx, workingDir, nil, args...); err != nil {
  36. return "", errors.Wrap(err, "git clone")
  37. }
  38. return repoPath, nil
  39. }
  40. func mirror(ctx context.Context, workingDir, repoURL string) (path string, err error) {
  41. repoPath := filepath.Join(workingDir, "repo")
  42. args := []string{"clone", "--mirror"}
  43. args = append(args, repoURL, repoPath)
  44. if err := execGitCmd(ctx, workingDir, nil, args...); err != nil {
  45. return "", errors.Wrap(err, "git clone --mirror")
  46. }
  47. return repoPath, nil
  48. }
  49. // checkPush sanity-checks that we can write to the upstream repo
  50. // (being able to `clone` is an adequate check that we can read the
  51. // upstream).
  52. func checkPush(ctx context.Context, workingDir, upstream string) error {
  53. // --force just in case we fetched the tag from upstream when cloning
  54. if err := execGitCmd(ctx, workingDir, nil, "tag", "--force", CheckPushTag); err != nil {
  55. return errors.Wrap(err, "tag for write check")
  56. }
  57. if err := execGitCmd(ctx, workingDir, nil, "push", "--force", upstream, "tag", CheckPushTag); err != nil {
  58. return errors.Wrap(err, "attempt to push tag")
  59. }
  60. return execGitCmd(ctx, workingDir, nil, "push", "--delete", upstream, "tag", CheckPushTag)
  61. }
  62. func commit(ctx context.Context, workingDir string, commitAction CommitAction) error {
  63. commitAuthor := commitAction.Author
  64. if commitAuthor != "" {
  65. if err := execGitCmd(ctx,
  66. workingDir, nil,
  67. "commit",
  68. "--no-verify", "-a", "--author", commitAuthor, "-m", commitAction.Message,
  69. ); err != nil {
  70. return errors.Wrap(err, "git commit")
  71. }
  72. return nil
  73. }
  74. if err := execGitCmd(ctx,
  75. workingDir, nil,
  76. "commit",
  77. "--no-verify", "-a", "-m", commitAction.Message,
  78. ); err != nil {
  79. return errors.Wrap(err, "git commit")
  80. }
  81. return nil
  82. }
  83. // push the refs given to the upstream repo
  84. func push(ctx context.Context, workingDir, upstream string, refs []string) error {
  85. args := append([]string{"push", upstream}, refs...)
  86. if err := execGitCmd(ctx, workingDir, nil, args...); err != nil {
  87. return errors.Wrap(err, fmt.Sprintf("git push %s %s", upstream, refs))
  88. }
  89. return nil
  90. }
  91. // fetch updates refs from the upstream.
  92. func fetch(ctx context.Context, workingDir, upstream string, refspec ...string) error {
  93. args := append([]string{"fetch", "--tags", upstream}, refspec...)
  94. if err := execGitCmd(ctx, workingDir, nil, args...); err != nil &&
  95. !strings.Contains(err.Error(), "Couldn't find remote ref") {
  96. return errors.Wrap(err, fmt.Sprintf("git fetch --tags %s %s", upstream, refspec))
  97. }
  98. return nil
  99. }
  100. func refExists(ctx context.Context, workingDir, ref string) (bool, error) {
  101. if err := execGitCmd(ctx, workingDir, nil, "rev-list", ref); err != nil {
  102. if strings.Contains(err.Error(), "unknown revision") {
  103. return false, nil
  104. }
  105. return false, err
  106. }
  107. return true, nil
  108. }
  109. // Get the full ref for a shorthand notes ref.
  110. func getNotesRef(ctx context.Context, workingDir, ref string) (string, error) {
  111. out := &bytes.Buffer{}
  112. if err := execGitCmd(ctx, workingDir, out, "notes", "--ref", ref, "get-ref"); err != nil {
  113. return "", err
  114. }
  115. return strings.TrimSpace(out.String()), nil
  116. }
  117. func addNote(ctx context.Context, workingDir, rev, notesRef string, note *Note) error {
  118. b, err := json.Marshal(note)
  119. if err != nil {
  120. return err
  121. }
  122. return execGitCmd(ctx, workingDir, nil, "notes", "--ref", notesRef, "add", "-m", string(b), rev)
  123. }
  124. // NB return values (*Note, nil), (nil, error), (nil, nil)
  125. func getNote(ctx context.Context, workingDir, notesRef, rev string) (*Note, error) {
  126. out := &bytes.Buffer{}
  127. if err := execGitCmd(ctx, workingDir, out, "notes", "--ref", notesRef, "show", rev); err != nil {
  128. if strings.Contains(strings.ToLower(err.Error()), "no note found for object") {
  129. return nil, nil
  130. }
  131. return nil, err
  132. }
  133. var note Note
  134. if err := json.NewDecoder(out).Decode(&note); err != nil {
  135. return nil, err
  136. }
  137. return &note, nil
  138. }
  139. // Get all revisions with a note (NB: DO NOT RELY ON THE ORDERING)
  140. // It appears to be ordered by ascending git object ref, not by time.
  141. // Return a map to make it easier to do "if in" type queries.
  142. func noteRevList(ctx context.Context, workingDir, notesRef string) (map[string]struct{}, error) {
  143. out := &bytes.Buffer{}
  144. if err := execGitCmd(ctx, workingDir, out, "notes", "--ref", notesRef, "list"); err != nil {
  145. return nil, err
  146. }
  147. noteList := splitList(out.String())
  148. result := make(map[string]struct{}, len(noteList))
  149. for _, l := range noteList {
  150. split := strings.Fields(l)
  151. if len(split) > 0 {
  152. result[split[1]] = struct{}{} // First field contains the object ref (commit id in our case)
  153. }
  154. }
  155. return result, nil
  156. }
  157. // Get the commit hash for a reference
  158. func refRevision(ctx context.Context, path, ref string) (string, error) {
  159. out := &bytes.Buffer{}
  160. if err := execGitCmd(ctx, path, out, "rev-list", "--max-count", "1", ref); err != nil {
  161. return "", err
  162. }
  163. return strings.TrimSpace(out.String()), nil
  164. }
  165. func revlist(ctx context.Context, path, ref string) ([]string, error) {
  166. out := &bytes.Buffer{}
  167. if err := execGitCmd(ctx, path, out, "rev-list", ref); err != nil {
  168. return nil, err
  169. }
  170. return splitList(out.String()), nil
  171. }
  172. // Return the revisions and one-line log commit messages
  173. // subdir argument ... corresponds to the git-path flag supplied to weave-flux-agent
  174. func onelinelog(ctx context.Context, path, refspec, subdir string) ([]Commit, error) {
  175. out := &bytes.Buffer{}
  176. // we need to distinguish whether subdir is populated or not,
  177. // because supplying an empty string to execGitCmd results in git complaining about
  178. // >> ambiguous argument '' <<
  179. if subdir != "" {
  180. if err := execGitCmd(ctx, path, out, "log", "--oneline", "--no-abbrev-commit", refspec, "--", subdir); err != nil {
  181. return nil, err
  182. }
  183. return splitLog(out.String())
  184. }
  185. if err := execGitCmd(ctx, path, out, "log", "--oneline", "--no-abbrev-commit", refspec); err != nil {
  186. return nil, err
  187. }
  188. return splitLog(out.String())
  189. }
  190. func splitLog(s string) ([]Commit, error) {
  191. lines := splitList(s)
  192. commits := make([]Commit, len(lines))
  193. for i, m := range lines {
  194. revAndMessage := strings.SplitN(m, " ", 2)
  195. commits[i].Revision = revAndMessage[0]
  196. commits[i].Message = revAndMessage[1]
  197. }
  198. return commits, nil
  199. }
  200. func splitList(s string) []string {
  201. outStr := strings.TrimSpace(s)
  202. if outStr == "" {
  203. return []string{}
  204. }
  205. return strings.Split(outStr, "\n")
  206. }
  207. // Move the tag to the ref given and push that tag upstream
  208. func moveTagAndPush(ctx context.Context, path string, tag, ref, msg, upstream string) error {
  209. if err := execGitCmd(ctx, path, nil, "tag", "--force", "-a", "-m", msg, tag, ref); err != nil {
  210. return errors.Wrap(err, "moving tag "+tag)
  211. }
  212. if err := execGitCmd(ctx, path, nil, "push", "--force", upstream, "tag", tag); err != nil {
  213. return errors.Wrap(err, "pushing tag to origin")
  214. }
  215. return nil
  216. }
  217. func changedFiles(ctx context.Context, path, subPath, ref string) ([]string, error) {
  218. // Remove leading slash if present. diff doesn't work when using github style root paths.
  219. if len(subPath) > 0 && subPath[0] == '/' {
  220. return []string{}, errors.New("git subdirectory should not have leading forward slash")
  221. }
  222. out := &bytes.Buffer{}
  223. // This uses --diff-filter to only look at changes for file _in
  224. // the working dir_; i.e, we do not report on things that no
  225. // longer appear.
  226. if err := execGitCmd(ctx, path, out, "diff", "--name-only", "--diff-filter=ACMRT", ref, "--", subPath); err != nil {
  227. return nil, err
  228. }
  229. return splitList(out.String()), nil
  230. }
  231. func execGitCmd(ctx context.Context, dir string, out io.Writer, args ...string) error {
  232. if trace {
  233. print("TRACE: git")
  234. for _, arg := range args {
  235. print(` "`, arg, `"`)
  236. }
  237. println()
  238. }
  239. c := exec.CommandContext(ctx, "git", args...)
  240. if dir != "" {
  241. c.Dir = dir
  242. }
  243. c.Env = env()
  244. c.Stdout = ioutil.Discard
  245. if out != nil {
  246. c.Stdout = out
  247. }
  248. errOut := &bytes.Buffer{}
  249. c.Stderr = errOut
  250. err := c.Run()
  251. if err != nil {
  252. msg := findErrorMessage(errOut)
  253. if msg != "" {
  254. err = errors.New(msg)
  255. }
  256. }
  257. if ctx.Err() == context.DeadlineExceeded {
  258. return errors.Wrap(ctx.Err(), fmt.Sprintf("running git command: %s %v", "git", args))
  259. } else if ctx.Err() == context.Canceled {
  260. return errors.Wrap(ctx.Err(), fmt.Sprintf("context was unexpectedly cancelled when running git command: %s %v", "git", args))
  261. }
  262. return err
  263. }
  264. func env() []string {
  265. return []string{"GIT_TERMINAL_PROMPT=0"}
  266. }
  267. // check returns true if there are changes locally.
  268. func check(ctx context.Context, workingDir, subdir string) bool {
  269. // `--quiet` means "exit with 1 if there are changes"
  270. return execGitCmd(ctx, workingDir, nil, "diff", "--quiet", "--", subdir) != nil
  271. }
  272. func findErrorMessage(output io.Reader) string {
  273. sc := bufio.NewScanner(output)
  274. for sc.Scan() {
  275. switch {
  276. case strings.HasPrefix(sc.Text(), "fatal: "):
  277. return sc.Text()
  278. case strings.HasPrefix(sc.Text(), "ERROR fatal: "): // Saw this error on ubuntu systems
  279. return sc.Text()
  280. case strings.HasPrefix(sc.Text(), "error:"):
  281. return strings.Trim(sc.Text(), "error: ")
  282. }
  283. }
  284. return ""
  285. }