internal/control/mirrorcmd.go

v1.32.1
gitbay/internal/control/mirrorcmd.go history · blame · raw

161 lines · 5030 bytes

  1package control
  2
  3import (
  4	"bufio"
  5	"errors"
  6	"fmt"
  7	"io"
  8	"strconv"
  9	"strings"
 10
 11	"gitbay.org/gitbay/internal/policy"
 12	"gitbay.org/gitbay/internal/protocol"
 13	"gitbay.org/gitbay/internal/store"
 14	"gitbay.org/gitbay/internal/webhook"
 15)
 16
 17func init() {
 18	register(Command{Path: []string{"repo", "mirror", "add"},
 19		Summary:    "mirror to or from a remote",
 20		Usage:      "repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]",
 21		ReadsStdin: true, Run: runMirrorAdd})
 22	register(Command{Path: []string{"repo", "mirror", "list"},
 23		Summary: "list mirrors with sync status",
 24		Usage:   "repo mirror list <owner/name>", ReadOnly: true, Run: runMirrorList})
 25	register(Command{Path: []string{"repo", "mirror", "remove"},
 26		Summary: "remove a mirror",
 27		Usage:   "repo mirror remove <owner/name> <id>", Run: runMirrorRemove})
 28	register(Command{Path: []string{"repo", "mirror", "sync"},
 29		Summary: "schedule an immediate sync",
 30		Usage:   "repo mirror sync <owner/name>", Run: runMirrorSync})
 31}
 32
 33func runMirrorAdd(c *Ctx, args []string) int {
 34	f, err := parseFlags(args, flagSpec{Values: []string{"--direction", "--username"}, Bools: []string{"--token-stdin"}, MaxPos: 2,
 35		Usage: "repo mirror add <owner/name> <url> --direction push|pull [--username <u>] [--token-stdin]"})
 36	if err != nil {
 37		return c.fail(protocol.ExitUsage, "%v", err)
 38	}
 39	path, urlArg := f.pos(0), f.pos(1)
 40	direction, username, tokenStdin := f.Value("--direction"), f.Value("--username"), f.Has("--token-stdin")
 41	if path == "" || urlArg == "" || (direction != "push" && direction != "pull") {
 42		return c.usage()
 43	}
 44	// The worker's git process dials this URL from the server: same SSRF
 45	// surface as a webhook target, same rules.
 46	if err := webhook.ValidateURL(urlArg, c.Cfg.Webhooks.AllowLocal); err != nil {
 47		return c.failInput(err)
 48	}
 49	repo, code := resolveRepo(c, path, policy.CanAdmin)
 50	if code >= 0 {
 51		return code
 52	}
 53	token := ""
 54	if tokenStdin {
 55		line, err := bufio.NewReader(io.LimitReader(c.Stdin, 4096)).ReadString('\n')
 56		if err != nil && line == "" {
 57			return c.fail(protocol.ExitUsage, "--token-stdin given but stdin held no token")
 58		}
 59		token = strings.TrimSpace(line)
 60	}
 61	id, err := c.Store.AddMirror(repo.ID, direction, urlArg, username, token)
 62	if err != nil {
 63		if errors.Is(err, store.ErrExists) {
 64			return c.fail(protocol.ExitUsage, "that mirror already exists")
 65		}
 66		return c.fail(protocol.ExitFailure, "%v", err)
 67	}
 68	note := ""
 69	if direction == "pull" {
 70		note = "; local pushes are now refused — refs come from the upstream"
 71	}
 72	return c.emit(map[string]any{"id": id, "direction": direction, "url": urlArg}, func(w io.Writer) {
 73		fmt.Fprintf(w, "mirror %d added (%s %s)%s\n", id, direction, urlArg, note)
 74	})
 75}
 76
 77func runMirrorList(c *Ctx, args []string) int {
 78	if len(args) != 1 {
 79		return c.usage()
 80	}
 81	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
 82	if code >= 0 {
 83		return code
 84	}
 85	ms, err := c.Store.ListMirrors(repo.ID)
 86	if err != nil {
 87		return c.fail(protocol.ExitFailure, "%v", err)
 88	}
 89	type out struct {
 90		ID        int64  `json:"id"`
 91		Direction string `json:"direction"`
 92		URL       string `json:"url"`
 93		Username  string `json:"username,omitempty"`
 94		Pending   bool   `json:"pending"`
 95		LastSync  string `json:"last_sync,omitempty"`
 96		LastError string `json:"last_error,omitempty"`
 97	}
 98	var ds []out
 99	for _, m := range ms {
100		// The token never leaves the server, in any encoding.
101		ds = append(ds, out{m.ID, m.Direction, m.URL, m.Username, m.Dirty, m.LastSync, m.LastError})
102	}
103	return c.emit(ds, func(w io.Writer) {
104		for _, d := range ds {
105			status := "ok"
106			if d.Pending {
107				status = "pending"
108			}
109			if d.LastError != "" {
110				status = "error: " + d.LastError
111			}
112			fmt.Fprintf(w, "%d\t%s\t%s\tlast %s\t%s\n", d.ID, d.Direction, d.URL, orDash(d.LastSync), status)
113		}
114	})
115}
116
117func orDash(s string) string {
118	if s == "" {
119		return "-"
120	}
121	return s
122}
123
124func runMirrorRemove(c *Ctx, args []string) int {
125	if len(args) != 2 {
126		return c.usage()
127	}
128	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
129	if code >= 0 {
130		return code
131	}
132	id, err := strconv.ParseInt(args[1], 10, 64)
133	if err != nil {
134		return c.fail(protocol.ExitUsage, "bad mirror id %q", args[1])
135	}
136	if err := c.Store.RemoveMirror(repo.ID, id); err != nil {
137		if errors.Is(err, store.ErrNotFound) {
138			return c.fail(protocol.ExitNotFound, "no mirror %d on %s", id, repo.Path())
139		}
140		return c.fail(protocol.ExitFailure, "%v", err)
141	}
142	return c.emit(map[string]any{"removed": id}, func(w io.Writer) {
143		fmt.Fprintf(w, "removed mirror %d\n", id)
144	})
145}
146
147func runMirrorSync(c *Ctx, args []string) int {
148	if len(args) != 1 {
149		return c.usage()
150	}
151	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
152	if code >= 0 {
153		return code
154	}
155	if err := c.Store.MarkMirrorsDirty(repo.ID, ""); err != nil {
156		return c.fail(protocol.ExitFailure, "%v", err)
157	}
158	return c.emit(map[string]string{"sync": "scheduled"}, func(w io.Writer) {
159		fmt.Fprintln(w, "sync scheduled; check repo mirror list for the outcome")
160	})
161}