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