internal/control/mirrorcmd.go

81bb0d14080e45ffcd6ca844cbe506ce1f10e22a
gitbay/internal/control/mirrorcmd.go history · blame · raw

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