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