internal/control/webhook.go

3af6fd1aaf69413d258a4131a627d048d2f1d290
gitbay/internal/control/webhook.go history · blame · raw

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}