internal/control/webhook.go
189 lines · 6197 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 f, err := parseFlags(args, flagSpec{Values: []string{"--secret", "--events"}, MaxPos: 2, Usage: "webhook add <owner/name> <url> [--secret <s>] [--events push,issue.created|*]"})
35 if err != nil {
36 return c.fail(protocol.ExitUsage, "%v", err)
37 }
38 path, url, secret, events := f.pos(0), f.pos(1), f.Value("--secret"), "*"
39 if f.Has("--events") {
40 events = f.Value("--events")
41 }
42 if path == "" || url == "" {
43 return c.fail(protocol.ExitUsage, "usage: webhook add <owner/name> <url> [--secret <s>] [--events <k1,k2>|*]")
44 }
45 repo, code := resolveRepo(c, path, policy.CanAdmin)
46 if code >= 0 {
47 return code
48 }
49 // A name that is not an event is a subscription that never fires, and
50 // nothing would ever say so. Checked before the URL, which resolves
51 // DNS: a typo here should not need a reachable host to report.
52 if code := checkEventNames(c, events); code >= 0 {
53 return code
54 }
55 if err := webhook.ValidateURL(url, c.Cfg.Webhooks.AllowLocal); err != nil {
56 return c.failErr(err)
57 }
58 id, err := c.Store.AddWebhook(repo.ID, url, secret, events)
59 if err != nil {
60 return c.fail(protocol.ExitFailure, "%v", err)
61 }
62 return c.emit(map[string]any{"id": id, "url": url, "events": events}, func(w io.Writer) {
63 fmt.Fprintf(w, "webhook %d added for %s (%s)\n", id, repo.Path(), events)
64 })
65}
66
67func runWebhookList(c *Ctx, args []string) int {
68 if len(args) != 1 {
69 return c.fail(protocol.ExitUsage, "usage: webhook list <owner/name>")
70 }
71 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
72 if code >= 0 {
73 return code
74 }
75 hooks, err := c.Store.ListWebhooks(repo.ID)
76 if err != nil {
77 return c.fail(protocol.ExitFailure, "%v", err)
78 }
79 type out struct {
80 ID int64 `json:"id"`
81 URL string `json:"url"`
82 Events string `json:"events"`
83 Active bool `json:"active"`
84 Secret bool `json:"has_secret"`
85 }
86 var ds []out
87 for _, h := range hooks {
88 ds = append(ds, out{h.ID, h.URL, h.Events, h.Active, h.Secret != ""})
89 }
90 return c.emit(ds, func(w io.Writer) {
91 for _, d := range ds {
92 fmt.Fprintf(w, "%d\t%s\t%s\n", d.ID, d.URL, d.Events)
93 }
94 })
95}
96
97func runWebhookRemove(c *Ctx, args []string) int {
98 if len(args) != 2 {
99 return c.fail(protocol.ExitUsage, "usage: webhook remove <owner/name> <id>")
100 }
101 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
102 if code >= 0 {
103 return code
104 }
105 id, err := strconv.ParseInt(args[1], 10, 64)
106 if err != nil {
107 return c.fail(protocol.ExitUsage, "bad webhook id %q", args[1])
108 }
109 if err := c.Store.RemoveWebhook(repo.ID, id); err != nil {
110 if errors.Is(err, store.ErrNotFound) {
111 return c.fail(protocol.ExitNotFound, "no webhook %d on %s", id, repo.Path())
112 }
113 return c.fail(protocol.ExitFailure, "%v", err)
114 }
115 return c.emit(map[string]any{"removed": id}, func(w io.Writer) {
116 fmt.Fprintf(w, "removed webhook %d\n", id)
117 })
118}
119
120func runWebhookDeliveries(c *Ctx, args []string) int {
121 f, err := parseFlags(args, flagSpec{Values: []string{"--limit"}, MaxPos: 1, Usage: "webhook deliveries <owner/name> [--limit n]"})
122 if err != nil {
123 return c.fail(protocol.ExitUsage, "%v", err)
124 }
125 limit, path := 20, f.pos(0)
126 if f.Has("--limit") {
127 n, err := strconv.Atoi(f.Value("--limit"))
128 if err != nil || n < 1 || n > 200 {
129 return c.fail(protocol.ExitUsage, "--limit must be 1..200")
130 }
131 limit = n
132 }
133 if path == "" {
134 return c.fail(protocol.ExitUsage, "usage: webhook deliveries <owner/name> [--limit n]")
135 }
136 repo, code := resolveRepo(c, path, policy.CanAdmin)
137 if code >= 0 {
138 return code
139 }
140 ds, err := c.Store.ListDeliveries(repo.ID, limit)
141 if err != nil {
142 return c.fail(protocol.ExitFailure, "%v", err)
143 }
144 type out struct {
145 ID int64 `json:"id"`
146 URL string `json:"url"`
147 Event string `json:"event"`
148 Status string `json:"status"`
149 Attempts int `json:"attempts"`
150 LastStatus int `json:"last_status,omitempty"`
151 LastError string `json:"last_error,omitempty"`
152 }
153 var rows []out
154 for _, d := range ds {
155 rows = append(rows, out{d.ID, d.URL, d.EventKind, d.Status, d.Attempts, d.LastStatus, d.LastError})
156 }
157 return c.emit(rows, func(w io.Writer) {
158 for _, d := range rows {
159 extra := ""
160 if d.LastError != "" {
161 extra = "\t" + d.LastError
162 }
163 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)
164 }
165 })
166}
167
168func runWebhookRedeliver(c *Ctx, args []string) int {
169 if len(args) != 2 {
170 return c.fail(protocol.ExitUsage, "usage: webhook redeliver <owner/name> <delivery-id>")
171 }
172 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
173 if code >= 0 {
174 return code
175 }
176 id, err := strconv.ParseInt(args[1], 10, 64)
177 if err != nil {
178 return c.fail(protocol.ExitUsage, "bad delivery id %q", args[1])
179 }
180 if err := c.Store.Redeliver(repo.ID, id); err != nil {
181 if errors.Is(err, store.ErrNotFound) {
182 return c.fail(protocol.ExitNotFound, "no delivery %d on %s", id, repo.Path())
183 }
184 return c.fail(protocol.ExitFailure, "%v", err)
185 }
186 return c.emit(map[string]any{"requeued": id}, func(w io.Writer) {
187 fmt.Fprintf(w, "delivery %d requeued\n", id)
188 })
189}