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