internal/control/webhook.go
207 lines · 6169 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|*]", Run: runWebhookAdd})
19 register(Command{Path: []string{"webhook", "list"},
20 Summary: "list webhooks",
21 Usage: "webhook list <owner/name>", ReadOnly: true, Run: runWebhookList})
22 register(Command{Path: []string{"webhook", "remove"},
23 Summary: "remove a webhook",
24 Usage: "webhook remove <owner/name> <id>", Run: runWebhookRemove})
25 register(Command{Path: []string{"webhook", "deliveries"},
26 Summary: "recent deliveries",
27 Usage: "webhook deliveries <owner/name> [--limit n]", ReadOnly: true, Run: runWebhookDeliveries})
28 register(Command{Path: []string{"webhook", "redeliver"},
29 Summary: "queue a delivery again",
30 Usage: "webhook redeliver <owner/name> <delivery-id>", Run: runWebhookRedeliver})
31}
32
33func runWebhookAdd(c *Ctx, args []string) int {
34 var path, url, secret string
35 events := "*"
36 for i := 0; i < len(args); i++ {
37 switch args[i] {
38 case "--secret", "--events":
39 if i+1 >= len(args) {
40 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
41 }
42 if args[i] == "--secret" {
43 secret = args[i+1]
44 } else {
45 events = args[i+1]
46 }
47 i++
48 default:
49 if path == "" {
50 path = args[i]
51 } else if url == "" {
52 url = args[i]
53 } else {
54 return c.fail(protocol.ExitUsage, "unexpected argument %q", args[i])
55 }
56 }
57 }
58 if path == "" || url == "" {
59 return c.fail(protocol.ExitUsage, "usage: webhook add <owner/name> <url> [--secret <s>] [--events <k1,k2>|*]")
60 }
61 repo, code := resolveRepo(c, path, policy.CanAdmin)
62 if code >= 0 {
63 return code
64 }
65 if err := webhook.ValidateURL(url, c.Cfg.Webhooks.AllowLocal); err != nil {
66 return c.fail(protocol.ExitUsage, "%v", err)
67 }
68 id, err := c.Store.AddWebhook(repo.ID, url, secret, events)
69 if err != nil {
70 return c.fail(protocol.ExitFailure, "%v", err)
71 }
72 return c.emit(map[string]any{"id": id, "url": url, "events": events}, func(w io.Writer) {
73 fmt.Fprintf(w, "webhook %d added for %s (%s)\n", id, repo.Path(), events)
74 })
75}
76
77func runWebhookList(c *Ctx, args []string) int {
78 if len(args) != 1 {
79 return c.fail(protocol.ExitUsage, "usage: webhook list <owner/name>")
80 }
81 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
82 if code >= 0 {
83 return code
84 }
85 hooks, err := c.Store.ListWebhooks(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 URL string `json:"url"`
92 Events string `json:"events"`
93 Active bool `json:"active"`
94 Secret bool `json:"has_secret"`
95 }
96 var ds []out
97 for _, h := range hooks {
98 ds = append(ds, out{h.ID, h.URL, h.Events, h.Active, h.Secret != ""})
99 }
100 return c.emit(ds, func(w io.Writer) {
101 for _, d := range ds {
102 fmt.Fprintf(w, "%d\t%s\t%s\n", d.ID, d.URL, d.Events)
103 }
104 })
105}
106
107func runWebhookRemove(c *Ctx, args []string) int {
108 if len(args) != 2 {
109 return c.fail(protocol.ExitUsage, "usage: webhook remove <owner/name> <id>")
110 }
111 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
112 if code >= 0 {
113 return code
114 }
115 id, err := strconv.ParseInt(args[1], 10, 64)
116 if err != nil {
117 return c.fail(protocol.ExitUsage, "bad webhook id %q", args[1])
118 }
119 if err := c.Store.RemoveWebhook(repo.ID, id); err != nil {
120 if errors.Is(err, store.ErrNotFound) {
121 return c.fail(protocol.ExitNotFound, "no webhook %d on %s", id, repo.Path())
122 }
123 return c.fail(protocol.ExitFailure, "%v", err)
124 }
125 return c.emit(map[string]any{"removed": id}, func(w io.Writer) {
126 fmt.Fprintf(w, "removed webhook %d\n", id)
127 })
128}
129
130func runWebhookDeliveries(c *Ctx, args []string) int {
131 limit := 20
132 var path string
133 for i := 0; i < len(args); i++ {
134 if args[i] == "--limit" {
135 if i+1 >= len(args) {
136 return c.fail(protocol.ExitUsage, "--limit requires a value")
137 }
138 n, err := strconv.Atoi(args[i+1])
139 if err != nil || n < 1 || n > 200 {
140 return c.fail(protocol.ExitUsage, "--limit must be 1..200")
141 }
142 limit = n
143 i++
144 continue
145 }
146 if path != "" {
147 return c.fail(protocol.ExitUsage, "usage: webhook deliveries <owner/name> [--limit n]")
148 }
149 path = args[i]
150 }
151 if path == "" {
152 return c.fail(protocol.ExitUsage, "usage: webhook deliveries <owner/name> [--limit n]")
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 for _, d := range rows {
177 extra := ""
178 if d.LastError != "" {
179 extra = "\t" + d.LastError
180 }
181 fmt.Fprintf(w, "%d\t%s\t%s\t%s (%d attempts)%s\n", d.ID, d.Event, d.URL, d.Status, d.Attempts, extra)
182 }
183 })
184}
185
186func runWebhookRedeliver(c *Ctx, args []string) int {
187 if len(args) != 2 {
188 return c.fail(protocol.ExitUsage, "usage: webhook redeliver <owner/name> <delivery-id>")
189 }
190 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
191 if code >= 0 {
192 return code
193 }
194 id, err := strconv.ParseInt(args[1], 10, 64)
195 if err != nil {
196 return c.fail(protocol.ExitUsage, "bad delivery id %q", args[1])
197 }
198 if err := c.Store.Redeliver(repo.ID, id); err != nil {
199 if errors.Is(err, store.ErrNotFound) {
200 return c.fail(protocol.ExitNotFound, "no delivery %d on %s", id, repo.Path())
201 }
202 return c.fail(protocol.ExitFailure, "%v", err)
203 }
204 return c.emit(map[string]any{"requeued": id}, func(w io.Writer) {
205 fmt.Fprintf(w, "delivery %d requeued\n", id)
206 })
207}