internal/control/webhook.go

aa03e8c8e02357cfe5f94188fdab3c79846cebae
gitbay/internal/control/webhook.go history · blame · raw

191 lines · 6354 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		// 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.fail(protocol.ExitUsage, "usage: webhook list <owner/name>")
 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.fail(protocol.ExitUsage, "usage: webhook remove <owner/name> <id>")
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.fail(protocol.ExitUsage, "usage: webhook deliveries <owner/name> [--limit n]")
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.fail(protocol.ExitUsage, "usage: webhook redeliver <owner/name> <delivery-id>")
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}