package control import ( "errors" "fmt" "io" "strconv" "gitbay.org/gitbay/internal/policy" "gitbay.org/gitbay/internal/protocol" "gitbay.org/gitbay/internal/store" "gitbay.org/gitbay/internal/webhook" ) func init() { register(Command{Path: []string{"webhook", "add"}, Summary: "add a webhook", Usage: "webhook add [--secret ] [--events push,issue.created|*]", Flags: []Flag{ {"--secret", "", "signs deliveries so the receiver can verify them", ""}, {"--events", "push,issue.created|*", "which events to send", "*"}, }, Examples: []string{"webhook add krz/gitbay https://ci.example.org/hook --events push"}, Run: runWebhookAdd}) register(Command{Path: []string{"webhook", "list"}, Summary: "list webhooks", Usage: "webhook list ", Examples: []string{"webhook list krz/gitbay"}, ReadOnly: true, Run: runWebhookList}) register(Command{Path: []string{"webhook", "remove"}, Summary: "remove a webhook", Usage: "webhook remove ", Examples: []string{"webhook remove krz/gitbay 3"}, Run: runWebhookRemove}) register(Command{Path: []string{"webhook", "deliveries"}, Summary: "recent deliveries", Usage: "webhook deliveries [--limit n]", Flags: []Flag{ {"--limit", "n", "rows to show", "20"}, }, Examples: []string{"webhook deliveries krz/gitbay --limit 50"}, ReadOnly: true, Run: runWebhookDeliveries}) register(Command{Path: []string{"webhook", "redeliver"}, Summary: "queue a delivery again", Usage: "webhook redeliver ", Examples: []string{"webhook redeliver krz/gitbay 42"}, Run: runWebhookRedeliver}) } func runWebhookAdd(c *Ctx, args []string) int { f, err := parseFlags(args, flagSpec{Values: []string{"--secret", "--events"}, MaxPos: 2, Usage: "webhook add [--secret ] [--events push,issue.created|*]"}) if err != nil { return c.fail(protocol.ExitUsage, "%v", err) } path, url, secret, events := f.pos(0), f.pos(1), f.Value("--secret"), "*" if f.Has("--events") { events = f.Value("--events") } if path == "" || url == "" { return c.usage() } repo, code := resolveRepo(c, path, policy.CanAdmin) if code >= 0 { return code } // A name that is not an event is a subscription that never fires, and // nothing would ever say so. Checked before the URL, which resolves // DNS: a typo here should not need a reachable host to report. if code := checkEventNames(c, events); code >= 0 { return code } if err := webhook.ValidateURL(url, c.Cfg.Webhooks.AllowLocal); err != nil { // The command line parsed; the value is what the server refuses. // Exit 1 carries the reason to every client verbatim (#187). return c.fail(protocol.ExitFailure, "%v", err) } id, err := c.Store.AddWebhook(repo.ID, url, secret, events) if err != nil { return c.fail(protocol.ExitFailure, "%v", err) } return c.emit(map[string]any{"id": id, "url": url, "events": events}, func(w io.Writer) { fmt.Fprintf(w, "webhook %d added for %s (%s)\n", id, repo.Path(), events) }) } func runWebhookList(c *Ctx, args []string) int { if len(args) != 1 { return c.usage() } repo, code := resolveRepo(c, args[0], policy.CanAdmin) if code >= 0 { return code } hooks, err := c.Store.ListWebhooks(repo.ID) if err != nil { return c.fail(protocol.ExitFailure, "%v", err) } type out struct { ID int64 `json:"id"` URL string `json:"url"` Events string `json:"events"` Active bool `json:"active"` Secret bool `json:"has_secret"` } var ds []out for _, h := range hooks { ds = append(ds, out{h.ID, h.URL, h.Events, h.Active, h.Secret != ""}) } return c.emit(ds, func(w io.Writer) { tb := c.table(w, "ID", "URL", "EVENTS") for _, d := range ds { tb.row(cRef(fmt.Sprintf("%d", d.ID)), cText(d.URL), cText(d.Events)) } tb.flush() }) } func runWebhookRemove(c *Ctx, args []string) int { if len(args) != 2 { return c.usage() } repo, code := resolveRepo(c, args[0], policy.CanAdmin) if code >= 0 { return code } id, err := strconv.ParseInt(args[1], 10, 64) if err != nil { return c.fail(protocol.ExitUsage, "bad webhook id %q", args[1]) } if err := c.Store.RemoveWebhook(repo.ID, id); err != nil { if errors.Is(err, store.ErrNotFound) { return c.fail(protocol.ExitNotFound, "no webhook %d on %s", id, repo.Path()) } return c.fail(protocol.ExitFailure, "%v", err) } return c.emit(map[string]any{"removed": id}, func(w io.Writer) { fmt.Fprintf(w, "removed webhook %d\n", id) }) } func runWebhookDeliveries(c *Ctx, args []string) int { f, err := parseFlags(args, flagSpec{Values: []string{"--limit"}, MaxPos: 1, Usage: "webhook deliveries [--limit n]"}) if err != nil { return c.fail(protocol.ExitUsage, "%v", err) } limit, path := 20, f.pos(0) if f.Has("--limit") { n, err := strconv.Atoi(f.Value("--limit")) if err != nil || n < 1 || n > 200 { return c.fail(protocol.ExitUsage, "--limit must be 1..200") } limit = n } if path == "" { return c.usage() } repo, code := resolveRepo(c, path, policy.CanAdmin) if code >= 0 { return code } ds, err := c.Store.ListDeliveries(repo.ID, limit) if err != nil { return c.fail(protocol.ExitFailure, "%v", err) } type out struct { ID int64 `json:"id"` URL string `json:"url"` Event string `json:"event"` Status string `json:"status"` Attempts int `json:"attempts"` LastStatus int `json:"last_status,omitempty"` LastError string `json:"last_error,omitempty"` } var rows []out for _, d := range ds { rows = append(rows, out{d.ID, d.URL, d.EventKind, d.Status, d.Attempts, d.LastStatus, d.LastError}) } return c.emit(rows, func(w io.Writer) { tb := c.table(w, "ID", "EVENT", "URL", "STATUS") for _, d := range rows { cells := []cell{cRef(fmt.Sprintf("%d", d.ID)), cText(d.Event), cText(d.URL), cState(fmt.Sprintf("%s (%d attempts)", d.Status, d.Attempts))} if d.LastError != "" { cells = append(cells, cText(d.LastError)) } tb.row(cells...) } tb.flush() }) } func runWebhookRedeliver(c *Ctx, args []string) int { if len(args) != 2 { return c.usage() } repo, code := resolveRepo(c, args[0], policy.CanAdmin) if code >= 0 { return code } id, err := strconv.ParseInt(args[1], 10, 64) if err != nil { return c.fail(protocol.ExitUsage, "bad delivery id %q", args[1]) } if err := c.Store.Redeliver(repo.ID, id); err != nil { if errors.Is(err, store.ErrNotFound) { return c.fail(protocol.ExitNotFound, "no delivery %d on %s", id, repo.Path()) } return c.fail(protocol.ExitFailure, "%v", err) } return c.emit(map[string]any{"requeued": id}, func(w io.Writer) { fmt.Fprintf(w, "delivery %d requeued\n", id) }) }