internal/control/mirrorcmd.go

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

185 lines · 5963 bytes

  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.emit(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	})
139}
140
141func orDash(s string) string {
142	if s == "" {
143		return "-"
144	}
145	return s
146}
147
148func runMirrorRemove(c *Ctx, args []string) int {
149	if len(args) != 2 {
150		return c.usage()
151	}
152	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
153	if code >= 0 {
154		return code
155	}
156	id, err := strconv.ParseInt(args[1], 10, 64)
157	if err != nil {
158		return c.fail(protocol.ExitUsage, "bad mirror id %q", args[1])
159	}
160	if err := c.Store.RemoveMirror(repo.ID, id); err != nil {
161		if errors.Is(err, store.ErrNotFound) {
162			return c.fail(protocol.ExitNotFound, "no mirror %d on %s", id, repo.Path())
163		}
164		return c.fail(protocol.ExitFailure, "%v", err)
165	}
166	return c.emit(map[string]any{"removed": id}, func(w io.Writer) {
167		fmt.Fprintf(w, "removed mirror %d\n", id)
168	})
169}
170
171func runMirrorSync(c *Ctx, args []string) int {
172	if len(args) != 1 {
173		return c.usage()
174	}
175	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
176	if code >= 0 {
177		return code
178	}
179	if err := c.Store.MarkMirrorsDirty(repo.ID, ""); err != nil {
180		return c.fail(protocol.ExitFailure, "%v", err)
181	}
182	return c.emit(map[string]string{"sync": "scheduled"}, func(w io.Writer) {
183		fmt.Fprintln(w, "sync scheduled; check repo mirror list for the outcome")
184	})
185}