internal/control/mirrorcmd.go

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

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