internal/control/mirrorcmd.go
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}