internal/control/webhook.go
210 lines · 6767 bytes
1package control
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "strconv"
8
9 "gitbay.org/gitbay/internal/policy"
10 "gitbay.org/gitbay/internal/protocol"
11 "gitbay.org/gitbay/internal/store"
12 "gitbay.org/gitbay/internal/webhook"
13)
14
15func init() {
16 register(Command{Path: []string{"webhook", "add"},
17 Summary: "add a webhook",
18 Usage: "webhook add <owner/name> <url> [--secret <s>] [--events push,issue.created|*]",
19 Flags: []Flag{
20 {"--secret", "<s>", "signs deliveries so the receiver can verify them", ""},
21 {"--events", "push,issue.created|*", "which events to send", "*"},
22 },
23 Examples: []string{"webhook add krz/gitbay https://ci.example.org/hook --events push"},
24 Run: runWebhookAdd})
25 register(Command{Path: []string{"webhook", "list"},
26 Summary: "list webhooks",
27 Usage: "webhook list <owner/name>",
28 Examples: []string{"webhook list krz/gitbay"}, ReadOnly: true, Run: runWebhookList})
29 register(Command{Path: []string{"webhook", "remove"},
30 Summary: "remove a webhook",
31 Usage: "webhook remove <owner/name> <id>",
32 Examples: []string{"webhook remove krz/gitbay 3"}, Run: runWebhookRemove})
33 register(Command{Path: []string{"webhook", "deliveries"},
34 Summary: "recent deliveries",
35 Usage: "webhook deliveries <owner/name> [--limit n]",
36 Flags: []Flag{
37 {"--limit", "n", "rows to show", "20"},
38 },
39 Examples: []string{"webhook deliveries krz/gitbay --limit 50"},
40 ReadOnly: true, Run: runWebhookDeliveries})
41 register(Command{Path: []string{"webhook", "redeliver"},
42 Summary: "queue a delivery again",
43 Usage: "webhook redeliver <owner/name> <delivery-id>",
44 Examples: []string{"webhook redeliver krz/gitbay 42"}, Run: runWebhookRedeliver})
45}
46
47func runWebhookAdd(c *Ctx, args []string) int {
48 f, err := parseFlags(args, flagSpec{Values: []string{"--secret", "--events"}, MaxPos: 2, Usage: "webhook add <owner/name> <url> [--secret <s>] [--events push,issue.created|*]"})
49 if err != nil {
50 return c.fail(protocol.ExitUsage, "%v", err)
51 }
52 path, url, secret, events := f.pos(0), f.pos(1), f.Value("--secret"), "*"
53 if f.Has("--events") {
54 events = f.Value("--events")
55 }
56 if path == "" || url == "" {
57 return c.usage()
58 }
59 repo, code := resolveRepo(c, path, policy.CanAdmin)
60 if code >= 0 {
61 return code
62 }
63 // A name that is not an event is a subscription that never fires, and
64 // nothing would ever say so. Checked before the URL, which resolves
65 // DNS: a typo here should not need a reachable host to report.
66 if code := checkEventNames(c, events); code >= 0 {
67 return code
68 }
69 if err := webhook.ValidateURL(url, c.Cfg.Webhooks.AllowLocal); err != nil {
70 // The command line parsed; the value is what the server refuses.
71 // Exit 1 carries the reason to every client verbatim (#187).
72 return c.fail(protocol.ExitFailure, "%v", err)
73 }
74 id, err := c.Store.AddWebhook(repo.ID, url, secret, events)
75 if err != nil {
76 return c.fail(protocol.ExitFailure, "%v", err)
77 }
78 return c.emit(map[string]any{"id": id, "url": url, "events": events}, func(w io.Writer) {
79 fmt.Fprintf(w, "webhook %d added for %s (%s)\n", id, repo.Path(), events)
80 })
81}
82
83func runWebhookList(c *Ctx, args []string) int {
84 if len(args) != 1 {
85 return c.usage()
86 }
87 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
88 if code >= 0 {
89 return code
90 }
91 hooks, err := c.Store.ListWebhooks(repo.ID)
92 if err != nil {
93 return c.fail(protocol.ExitFailure, "%v", err)
94 }
95 type out struct {
96 ID int64 `json:"id"`
97 URL string `json:"url"`
98 Events string `json:"events"`
99 Active bool `json:"active"`
100 Secret bool `json:"has_secret"`
101 }
102 var ds []out
103 for _, h := range hooks {
104 ds = append(ds, out{h.ID, h.URL, h.Events, h.Active, h.Secret != ""})
105 }
106 return c.emit(ds, func(w io.Writer) {
107 tb := c.table(w, "ID", "URL", "EVENTS")
108 for _, d := range ds {
109 tb.row(cRef(fmt.Sprintf("%d", d.ID)), cText(d.URL), cText(d.Events))
110 }
111 tb.flush()
112 })
113}
114
115func runWebhookRemove(c *Ctx, args []string) int {
116 if len(args) != 2 {
117 return c.usage()
118 }
119 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
120 if code >= 0 {
121 return code
122 }
123 id, err := strconv.ParseInt(args[1], 10, 64)
124 if err != nil {
125 return c.fail(protocol.ExitUsage, "bad webhook id %q", args[1])
126 }
127 if err := c.Store.RemoveWebhook(repo.ID, id); err != nil {
128 if errors.Is(err, store.ErrNotFound) {
129 return c.fail(protocol.ExitNotFound, "no webhook %d on %s", id, repo.Path())
130 }
131 return c.fail(protocol.ExitFailure, "%v", err)
132 }
133 return c.emit(map[string]any{"removed": id}, func(w io.Writer) {
134 fmt.Fprintf(w, "removed webhook %d\n", id)
135 })
136}
137
138func runWebhookDeliveries(c *Ctx, args []string) int {
139 f, err := parseFlags(args, flagSpec{Values: []string{"--limit"}, MaxPos: 1, Usage: "webhook deliveries <owner/name> [--limit n]"})
140 if err != nil {
141 return c.fail(protocol.ExitUsage, "%v", err)
142 }
143 limit, path := 20, f.pos(0)
144 if f.Has("--limit") {
145 n, err := strconv.Atoi(f.Value("--limit"))
146 if err != nil || n < 1 || n > 200 {
147 return c.fail(protocol.ExitUsage, "--limit must be 1..200")
148 }
149 limit = n
150 }
151 if path == "" {
152 return c.usage()
153 }
154 repo, code := resolveRepo(c, path, policy.CanAdmin)
155 if code >= 0 {
156 return code
157 }
158 ds, err := c.Store.ListDeliveries(repo.ID, limit)
159 if err != nil {
160 return c.fail(protocol.ExitFailure, "%v", err)
161 }
162 type out struct {
163 ID int64 `json:"id"`
164 URL string `json:"url"`
165 Event string `json:"event"`
166 Status string `json:"status"`
167 Attempts int `json:"attempts"`
168 LastStatus int `json:"last_status,omitempty"`
169 LastError string `json:"last_error,omitempty"`
170 }
171 var rows []out
172 for _, d := range ds {
173 rows = append(rows, out{d.ID, d.URL, d.EventKind, d.Status, d.Attempts, d.LastStatus, d.LastError})
174 }
175 return c.emit(rows, func(w io.Writer) {
176 tb := c.table(w, "ID", "EVENT", "URL", "STATUS")
177 for _, d := range rows {
178 cells := []cell{cRef(fmt.Sprintf("%d", d.ID)), cText(d.Event), cText(d.URL),
179 cState(fmt.Sprintf("%s (%d attempts)", d.Status, d.Attempts))}
180 if d.LastError != "" {
181 cells = append(cells, cText(d.LastError))
182 }
183 tb.row(cells...)
184 }
185 tb.flush()
186 })
187}
188
189func runWebhookRedeliver(c *Ctx, args []string) int {
190 if len(args) != 2 {
191 return c.usage()
192 }
193 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
194 if code >= 0 {
195 return code
196 }
197 id, err := strconv.ParseInt(args[1], 10, 64)
198 if err != nil {
199 return c.fail(protocol.ExitUsage, "bad delivery id %q", args[1])
200 }
201 if err := c.Store.Redeliver(repo.ID, id); err != nil {
202 if errors.Is(err, store.ErrNotFound) {
203 return c.fail(protocol.ExitNotFound, "no delivery %d on %s", id, repo.Path())
204 }
205 return c.fail(protocol.ExitFailure, "%v", err)
206 }
207 return c.emit(map[string]any{"requeued": id}, func(w io.Writer) {
208 fmt.Fprintf(w, "delivery %d requeued\n", id)
209 })
210}