internal/control/mirrorcmd.go

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

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