internal/control/mirrorcmd.go

e6cd75b5f28bacf51620bb531320c30fd4e66bfd
gitbay/internal/control/mirrorcmd.go history · blame · raw

205 lines · 6605 bytes

6 symbols in this file
  1package control
  2
  3import (
  4	"bufio"
  5	"errors"
  6	"fmt"
  7	"io"
  8	"net/url"
  9	"strconv"
 10	"strings"
 11
 12	"gitbay.org/gitbay/internal/gitpin"
 13	"gitbay.org/gitbay/internal/policy"
 14	"gitbay.org/gitbay/internal/protocol"
 15	"gitbay.org/gitbay/internal/store"
 16	"gitbay.org/gitbay/internal/webhook"
 17)
 18
 19func init() {
 20	register(Command{Path: []string{"repo", "mirror", "add"},
 21		NeedsRecentSignIn: true,
 22		Summary:           "mirror to or from a remote",
 23		Usage:             "repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]",
 24		Flags: []Flag{
 25			{"--direction", "push|pull", "which way the mirror syncs", ""},
 26			{"--username", "<u>", "the remote's username", ""},
 27			{"--token-stdin", "", "read a credential token from stdin", ""},
 28		},
 29		Examples:   []string{"repo mirror add krz/gitbay https://github.com/krazywarez/gitbay.git --direction push"},
 30		ReadsStdin: true, Run: runMirrorAdd})
 31	register(Command{Path: []string{"repo", "mirror", "list"},
 32		Summary:  "list mirrors with sync status",
 33		Usage:    "repo mirror list <owner/name>",
 34		Examples: []string{"repo mirror list krz/gitbay"},
 35		ReadOnly: true, Run: runMirrorList})
 36	register(Command{Path: []string{"repo", "mirror", "remove"},
 37		Summary:  "remove a mirror",
 38		Usage:    "repo mirror remove <owner/name> <id>",
 39		Examples: []string{"repo mirror remove krz/gitbay 3"},
 40		Run:      runMirrorRemove})
 41	register(Command{Path: []string{"repo", "mirror", "sync"},
 42		Summary:  "schedule an immediate sync",
 43		Usage:    "repo mirror sync <owner/name>",
 44		Examples: []string{"repo mirror sync krz/gitbay"},
 45		Run:      runMirrorSync})
 46}
 47
 48func runMirrorAdd(c *Ctx, args []string) int {
 49	f, err := c.parseArgs(args, flagSpec{Values: []string{"--direction", "--username"}, Bools: []string{"--token-stdin"}, MaxPos: 2,
 50		Usage: "repo mirror add <owner/name> <url> --direction push|pull [--username <u>] [--token-stdin]"})
 51	if err != nil {
 52		return c.fail(protocol.ExitUsage, "%v", err)
 53	}
 54	path, urlArg := f.pos(0), f.pos(1)
 55	direction, username, tokenStdin := f.Value("--direction"), f.Value("--username"), f.Has("--token-stdin")
 56	if path == "" || urlArg == "" || (direction != "push" && direction != "pull") {
 57		return c.usage()
 58	}
 59	// Sync refuses a numerically written host; say so now, before a
 60	// resolver gets to read it.
 61	if u, err := url.Parse(urlArg); err == nil {
 62		if err := gitpin.CheckHost(u.Hostname()); err != nil {
 63			return c.failInput(err)
 64		}
 65	}
 66	// The worker's git process dials this URL from the server: same SSRF
 67	// surface as a webhook target, same rules.
 68	if err := webhook.ValidateURL(urlArg, c.Cfg.Webhooks.AllowLocal); err != nil {
 69		return c.failInput(err)
 70	}
 71	repo, code := resolveRepo(c, path, policy.CanAdmin)
 72	if code >= 0 {
 73		return code
 74	}
 75	token := ""
 76	if tokenStdin {
 77		line, err := bufio.NewReader(io.LimitReader(c.Stdin, 4096)).ReadString('\n')
 78		if err != nil && line == "" {
 79			return c.fail(protocol.ExitUsage, "--token-stdin given but stdin held no token")
 80		}
 81		token = strings.TrimSpace(line)
 82	}
 83	id, err := c.Store.AddMirror(repo.ID, direction, urlArg, username, token)
 84	if err != nil {
 85		if errors.Is(err, store.ErrExists) {
 86			return c.fail(protocol.ExitUsage, "that mirror already exists")
 87		}
 88		return c.fail(protocol.ExitFailure, "%v", err)
 89	}
 90	note := ""
 91	if direction == "pull" {
 92		note = "; local pushes are now refused — refs come from the upstream"
 93	}
 94	return c.emit(map[string]any{"id": id, "direction": direction, "url": urlArg}, func(w io.Writer) {
 95		fmt.Fprintf(w, "mirror %d added (%s %s)%s\n", id, direction, urlArg, note)
 96	})
 97}
 98
 99func runMirrorList(c *Ctx, args []string) int {
100	if len(args) != 1 {
101		return c.usage()
102	}
103	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
104	if code >= 0 {
105		return code
106	}
107	ms, err := c.Store.ListMirrors(repo.ID)
108	if err != nil {
109		return c.fail(protocol.ExitFailure, "%v", err)
110	}
111	type out struct {
112		ID        int64  `json:"id"`
113		Direction string `json:"direction"`
114		URL       string `json:"url"`
115		Username  string `json:"username,omitempty"`
116		Pending   bool   `json:"pending"`
117		LastSync  string `json:"last_sync,omitempty"`
118		LastError string `json:"last_error,omitempty"`
119	}
120	var ds []out
121	for _, m := range ms {
122		// The token never leaves the server, in any encoding.
123		ds = append(ds, out{m.ID, m.Direction, m.URL, m.Username, m.Dirty, m.LastSync, m.LastError})
124	}
125	return c.emitView(ds, func(w io.Writer) {
126		tb := c.table(w, "ID", "DIRECTION", "URL", "LAST", "STATUS")
127		for _, d := range ds {
128			status := "ok"
129			if d.Pending {
130				status = "pending"
131			}
132			if d.LastError != "" {
133				status = "error: " + d.LastError
134			}
135			tb.row(cRef(fmt.Sprintf("%d", d.ID)), cText(d.Direction), cText(d.URL), cText("last "+orDash(d.LastSync)), cState(status))
136		}
137		tb.flush()
138	}, func() screen {
139		rows := make([]row, len(ds))
140		for i, d := range ds {
141			state := "ok"
142			if d.Pending {
143				state = "pending"
144			}
145			if d.LastError != "" {
146				state = "error"
147			}
148			synced := "never synced"
149			if d.LastSync != "" {
150				synced = "synced " + relAge(d.LastSync, termNow())
151			}
152			rows[i] = rowOf(cRef(strconv.FormatInt(d.ID, 10)), cGlyph(state), cFlex(d.URL), cMeta(d.Direction, synced), cMark(d.LastError, sgrRed))
153		}
154		return listScreen("Mirrors", rows,
155			action{"Mirrors", []string{"repo", "mirror", "sync", repo.Path()}},
156			action{"Mirrors", []string{"repo", "mirror", "remove", repo.Path(), "<id>"}},
157		)
158	})
159}
160
161func orDash(s string) string {
162	if s == "" {
163		return "-"
164	}
165	return s
166}
167
168func runMirrorRemove(c *Ctx, args []string) int {
169	if len(args) != 2 {
170		return c.usage()
171	}
172	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
173	if code >= 0 {
174		return code
175	}
176	id, err := strconv.ParseInt(args[1], 10, 64)
177	if err != nil {
178		return c.fail(protocol.ExitUsage, "bad mirror id %q", args[1])
179	}
180	if err := c.Store.RemoveMirror(repo.ID, id); err != nil {
181		if errors.Is(err, store.ErrNotFound) {
182			return c.fail(protocol.ExitNotFound, "no mirror %d on %s", id, repo.Path())
183		}
184		return c.fail(protocol.ExitFailure, "%v", err)
185	}
186	return c.emit(map[string]any{"removed": id}, func(w io.Writer) {
187		fmt.Fprintf(w, "removed mirror %d\n", id)
188	})
189}
190
191func runMirrorSync(c *Ctx, args []string) int {
192	if len(args) != 1 {
193		return c.usage()
194	}
195	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
196	if code >= 0 {
197		return code
198	}
199	if err := c.Store.MarkMirrorsDirty(repo.ID, ""); err != nil {
200		return c.fail(protocol.ExitFailure, "%v", err)
201	}
202	return c.emit(map[string]string{"sync": "scheduled"}, func(w io.Writer) {
203		fmt.Fprintln(w, "sync scheduled; check repo mirror list for the outcome")
204	})
205}