internal/control/webhook.go
191 lines · 6004 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.usage()
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 // The command line parsed; the value is what the server refuses.
57 // Exit 1 carries the reason to every client verbatim (#187).
58 return c.fail(protocol.ExitFailure, "%v", err)
59 }
60 id, err := c.Store.AddWebhook(repo.ID, url, secret, events)
61 if err != nil {
62 return c.fail(protocol.ExitFailure, "%v", err)
63 }
64 return c.emit(map[string]any{"id": id, "url": url, "events": events}, func(w io.Writer) {
65 fmt.Fprintf(w, "webhook %d added for %s (%s)\n", id, repo.Path(), events)
66 })
67}
68
69func runWebhookList(c *Ctx, args []string) int {
70 if len(args) != 1 {
71 return c.usage()
72 }
73 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
74 if code >= 0 {
75 return code
76 }
77 hooks, err := c.Store.ListWebhooks(repo.ID)
78 if err != nil {
79 return c.fail(protocol.ExitFailure, "%v", err)
80 }
81 type out struct {
82 ID int64 `json:"id"`
83 URL string `json:"url"`
84 Events string `json:"events"`
85 Active bool `json:"active"`
86 Secret bool `json:"has_secret"`
87 }
88 var ds []out
89 for _, h := range hooks {
90 ds = append(ds, out{h.ID, h.URL, h.Events, h.Active, h.Secret != ""})
91 }
92 return c.emit(ds, func(w io.Writer) {
93 for _, d := range ds {
94 fmt.Fprintf(w, "%d\t%s\t%s\n", d.ID, d.URL, d.Events)
95 }
96 })
97}
98
99func runWebhookRemove(c *Ctx, args []string) int {
100 if len(args) != 2 {
101 return c.usage()
102 }
103 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
104 if code >= 0 {
105 return code
106 }
107 id, err := strconv.ParseInt(args[1], 10, 64)
108 if err != nil {
109 return c.fail(protocol.ExitUsage, "bad webhook id %q", args[1])
110 }
111 if err := c.Store.RemoveWebhook(repo.ID, id); err != nil {
112 if errors.Is(err, store.ErrNotFound) {
113 return c.fail(protocol.ExitNotFound, "no webhook %d on %s", id, repo.Path())
114 }
115 return c.fail(protocol.ExitFailure, "%v", err)
116 }
117 return c.emit(map[string]any{"removed": id}, func(w io.Writer) {
118 fmt.Fprintf(w, "removed webhook %d\n", id)
119 })
120}
121
122func runWebhookDeliveries(c *Ctx, args []string) int {
123 f, err := parseFlags(args, flagSpec{Values: []string{"--limit"}, MaxPos: 1, Usage: "webhook deliveries <owner/name> [--limit n]"})
124 if err != nil {
125 return c.fail(protocol.ExitUsage, "%v", err)
126 }
127 limit, path := 20, f.pos(0)
128 if f.Has("--limit") {
129 n, err := strconv.Atoi(f.Value("--limit"))
130 if err != nil || n < 1 || n > 200 {
131 return c.fail(protocol.ExitUsage, "--limit must be 1..200")
132 }
133 limit = n
134 }
135 if path == "" {
136 return c.usage()
137 }
138 repo, code := resolveRepo(c, path, policy.CanAdmin)
139 if code >= 0 {
140 return code
141 }
142 ds, err := c.Store.ListDeliveries(repo.ID, limit)
143 if err != nil {
144 return c.fail(protocol.ExitFailure, "%v", err)
145 }
146 type out struct {
147 ID int64 `json:"id"`
148 URL string `json:"url"`
149 Event string `json:"event"`
150 Status string `json:"status"`
151 Attempts int `json:"attempts"`
152 LastStatus int `json:"last_status,omitempty"`
153 LastError string `json:"last_error,omitempty"`
154 }
155 var rows []out
156 for _, d := range ds {
157 rows = append(rows, out{d.ID, d.URL, d.EventKind, d.Status, d.Attempts, d.LastStatus, d.LastError})
158 }
159 return c.emit(rows, func(w io.Writer) {
160 for _, d := range rows {
161 extra := ""
162 if d.LastError != "" {
163 extra = "\t" + d.LastError
164 }
165 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)
166 }
167 })
168}
169
170func runWebhookRedeliver(c *Ctx, args []string) int {
171 if len(args) != 2 {
172 return c.usage()
173 }
174 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
175 if code >= 0 {
176 return code
177 }
178 id, err := strconv.ParseInt(args[1], 10, 64)
179 if err != nil {
180 return c.fail(protocol.ExitUsage, "bad delivery id %q", args[1])
181 }
182 if err := c.Store.Redeliver(repo.ID, id); err != nil {
183 if errors.Is(err, store.ErrNotFound) {
184 return c.fail(protocol.ExitNotFound, "no delivery %d on %s", id, repo.Path())
185 }
186 return c.fail(protocol.ExitFailure, "%v", err)
187 }
188 return c.emit(map[string]any{"requeued": id}, func(w io.Writer) {
189 fmt.Fprintf(w, "delivery %d requeued\n", id)
190 })
191}