1package cmd
2
3import (
4 "fmt"
5 "strconv"
6 "strings"
7
8 "github.com/charmbracelet/lipgloss/v2/table"
9 "github.com/charmbracelet/soft-serve/pkg/backend"
10 "github.com/charmbracelet/soft-serve/pkg/webhook"
11 "github.com/dustin/go-humanize"
12 "github.com/google/uuid"
13 "github.com/spf13/cobra"
14)
15
16func webhookCommand() *cobra.Command {
17 cmd := &cobra.Command{
18 Use: "webhook",
19 Aliases: []string{"webhooks"},
20 Short: "Manage repository webhooks",
21 }
22
23 cmd.AddCommand(
24 webhookListCommand(),
25 webhookCreateCommand(),
26 webhookDeleteCommand(),
27 webhookUpdateCommand(),
28 webhookDeliveriesCommand(),
29 )
30
31 return cmd
32}
33
34var webhookEvents []string
35
36func init() {
37 events := webhook.Events()
38 webhookEvents = make([]string, len(events))
39 for i, e := range events {
40 webhookEvents[i] = e.String()
41 }
42}
43
44func webhookListCommand() *cobra.Command {
45 cmd := &cobra.Command{
46 Use: "list REPOSITORY",
47 Short: "List repository webhooks",
48 Args: cobra.ExactArgs(1),
49 PersistentPreRunE: checkIfAdmin,
50 RunE: func(cmd *cobra.Command, args []string) error {
51 ctx := cmd.Context()
52 be := backend.FromContext(ctx)
53 repo, err := be.Repository(ctx, args[0])
54 if err != nil {
55 return err //nolint:wrapcheck
56 }
57
58 webhooks, err := be.ListWebhooks(ctx, repo)
59 if err != nil {
60 return err //nolint:wrapcheck
61 }
62
63 table := table.New().Headers("ID", "URL", "Events", "Active", "Created At", "Updated At")
64 for _, h := range webhooks {
65 events := make([]string, len(h.Events))
66 for i, e := range h.Events {
67 events[i] = e.String()
68 }
69
70 table = table.Row(
71 strconv.FormatInt(h.ID, 10),
72 h.URL,
73 strings.Join(events, ","),
74 strconv.FormatBool(h.Active),
75 humanize.Time(h.CreatedAt),
76 humanize.Time(h.UpdatedAt),
77 )
78 }
79 cmd.Println(table)
80 return nil
81 },
82 }
83
84 return cmd
85}
86
87func webhookCreateCommand() *cobra.Command {
88 var events []string
89 var secret string
90 var active bool
91 var contentType string
92 cmd := &cobra.Command{
93 Use: "create REPOSITORY URL",
94 Short: "Create a repository webhook",
95 Args: cobra.ExactArgs(2),
96 PersistentPreRunE: checkIfAdmin,
97 RunE: func(cmd *cobra.Command, args []string) error {
98 ctx := cmd.Context()
99 be := backend.FromContext(ctx)
100 repo, err := be.Repository(ctx, args[0])
101 if err != nil {
102 return err //nolint:wrapcheck
103 }
104
105 var evs []webhook.Event
106 for _, e := range events {
107 ev, err := webhook.ParseEvent(e)
108 if err != nil {
109 return fmt.Errorf("invalid event: %w", err)
110 }
111
112 evs = append(evs, ev)
113 }
114
115 var ct webhook.ContentType
116 switch strings.ToLower(strings.TrimSpace(contentType)) {
117 case "json":
118 ct = webhook.ContentTypeJSON
119 case "form":
120 ct = webhook.ContentTypeForm
121 default:
122 return webhook.ErrInvalidContentType
123 }
124
125 return be.CreateWebhook(ctx, repo, strings.TrimSpace(args[1]), ct, secret, evs, active)
126 },
127 }
128
129 cmd.Flags().StringSliceVarP(&events, "events", "e", nil, fmt.Sprintf("events to trigger the webhook, available events are (%s)", strings.Join(webhookEvents, ", ")))
130 cmd.Flags().StringVarP(&secret, "secret", "s", "", "secret to sign the webhook payload")
131 cmd.Flags().BoolVarP(&active, "active", "a", true, "whether the webhook is active")
132 cmd.Flags().StringVarP(&contentType, "content-type", "c", "json", "content type of the webhook payload, can be either `json` or `form`")
133
134 return cmd
135}
136
137func webhookDeleteCommand() *cobra.Command {
138 cmd := &cobra.Command{
139 Use: "delete REPOSITORY WEBHOOK_ID",
140 Short: "Delete a repository webhook",
141 Args: cobra.ExactArgs(2),
142 PersistentPreRunE: checkIfAdmin,
143 RunE: func(cmd *cobra.Command, args []string) error {
144 ctx := cmd.Context()
145 be := backend.FromContext(ctx)
146 repo, err := be.Repository(ctx, args[0])
147 if err != nil {
148 return err //nolint:wrapcheck
149 }
150
151 id, err := strconv.ParseInt(args[1], 10, 64)
152 if err != nil {
153 return fmt.Errorf("invalid webhook ID: %w", err)
154 }
155
156 return be.DeleteWebhook(ctx, repo, id)
157 },
158 }
159
160 return cmd
161}
162
163func webhookUpdateCommand() *cobra.Command {
164 var events []string
165 var secret string
166 var active string
167 var contentType string
168 var url string
169 cmd := &cobra.Command{
170 Use: "update REPOSITORY WEBHOOK_ID",
171 Short: "Update a repository webhook",
172 Args: cobra.ExactArgs(2),
173 PersistentPreRunE: checkIfAdmin,
174 RunE: func(cmd *cobra.Command, args []string) error {
175 ctx := cmd.Context()
176 be := backend.FromContext(ctx)
177 repo, err := be.Repository(ctx, args[0])
178 if err != nil {
179 return err //nolint:wrapcheck
180 }
181
182 id, err := strconv.ParseInt(args[1], 10, 64)
183 if err != nil {
184 return fmt.Errorf("invalid webhook ID: %w", err)
185 }
186
187 wh, err := be.Webhook(ctx, repo, id)
188 if err != nil {
189 return err //nolint:wrapcheck
190 }
191
192 newURL := wh.URL
193 if url != "" {
194 newURL = url
195 }
196
197 newSecret := wh.Secret
198 if secret != "" {
199 newSecret = secret
200 }
201
202 newActive := wh.Active
203 if active != "" {
204 active, err := strconv.ParseBool(active)
205 if err != nil {
206 return fmt.Errorf("invalid active value: %w", err)
207 }
208
209 newActive = active
210 }
211
212 newContentType := wh.ContentType
213 if contentType != "" {
214 var ct webhook.ContentType
215 switch strings.ToLower(strings.TrimSpace(contentType)) {
216 case "json":
217 ct = webhook.ContentTypeJSON
218 case "form":
219 ct = webhook.ContentTypeForm
220 default:
221 return webhook.ErrInvalidContentType
222 }
223 newContentType = ct
224 }
225
226 newEvents := wh.Events
227 if len(events) > 0 {
228 var evs []webhook.Event
229 for _, e := range events {
230 ev, err := webhook.ParseEvent(e)
231 if err != nil {
232 return fmt.Errorf("invalid event: %w", err)
233 }
234
235 evs = append(evs, ev)
236 }
237
238 newEvents = evs
239 }
240
241 return be.UpdateWebhook(ctx, repo, id, newURL, newContentType, newSecret, newEvents, newActive)
242 },
243 }
244
245 cmd.Flags().StringSliceVarP(&events, "events", "e", nil, fmt.Sprintf("events to trigger the webhook, available events are (%s)", strings.Join(webhookEvents, ", ")))
246 cmd.Flags().StringVarP(&secret, "secret", "s", "", "secret to sign the webhook payload")
247 cmd.Flags().StringVarP(&active, "active", "a", "", "whether the webhook is active")
248 cmd.Flags().StringVarP(&contentType, "content-type", "c", "", "content type of the webhook payload, can be either `json` or `form`")
249 cmd.Flags().StringVarP(&url, "url", "u", "", "webhook URL")
250
251 return cmd
252}
253
254func webhookDeliveriesCommand() *cobra.Command {
255 cmd := &cobra.Command{
256 Use: "deliveries",
257 Short: "Manage webhook deliveries",
258 Aliases: []string{"delivery", "deliver"},
259 }
260
261 cmd.AddCommand(
262 webhookDeliveriesListCommand(),
263 webhookDeliveriesRedeliverCommand(),
264 webhookDeliveriesGetCommand(),
265 )
266
267 return cmd
268}
269
270func webhookDeliveriesListCommand() *cobra.Command {
271 cmd := &cobra.Command{
272 Use: "list REPOSITORY WEBHOOK_ID",
273 Short: "List webhook deliveries",
274 Args: cobra.ExactArgs(2),
275 PersistentPreRunE: checkIfAdmin,
276 RunE: func(cmd *cobra.Command, args []string) error {
277 ctx := cmd.Context()
278 be := backend.FromContext(ctx)
279 id, err := strconv.ParseInt(args[1], 10, 64)
280 if err != nil {
281 return fmt.Errorf("invalid webhook ID: %w", err)
282 }
283
284 dels, err := be.ListWebhookDeliveries(ctx, id)
285 if err != nil {
286 return err //nolint:wrapcheck
287 }
288
289 table := table.New().Headers("Status", "ID", "Event", "Created At")
290 for _, d := range dels {
291 status := "❌"
292 if d.ResponseStatus >= 200 && d.ResponseStatus < 300 {
293 status = "✅"
294 }
295 table = table.Row(
296 status,
297 d.ID.String(),
298 d.Event.String(),
299 humanize.Time(d.CreatedAt),
300 )
301 }
302 cmd.Println(table)
303 return nil
304 },
305 }
306
307 return cmd
308}
309
310func webhookDeliveriesRedeliverCommand() *cobra.Command {
311 cmd := &cobra.Command{
312 Use: "redeliver REPOSITORY WEBHOOK_ID DELIVERY_ID",
313 Short: "Redeliver a webhook delivery",
314 PersistentPreRunE: checkIfAdmin,
315 RunE: func(cmd *cobra.Command, args []string) error {
316 ctx := cmd.Context()
317 be := backend.FromContext(ctx)
318 repo, err := be.Repository(ctx, args[0])
319 if err != nil {
320 return err //nolint:wrapcheck
321 }
322
323 id, err := strconv.ParseInt(args[1], 10, 64)
324 if err != nil {
325 return fmt.Errorf("invalid webhook ID: %w", err)
326 }
327
328 delID, err := uuid.Parse(args[2])
329 if err != nil {
330 return fmt.Errorf("invalid delivery ID: %w", err)
331 }
332
333 return be.RedeliverWebhookDelivery(ctx, repo, id, delID)
334 },
335 }
336
337 return cmd
338}
339
340func webhookDeliveriesGetCommand() *cobra.Command {
341 cmd := &cobra.Command{
342 Use: "get REPOSITORY WEBHOOK_ID DELIVERY_ID",
343 Short: "Get a webhook delivery",
344 PersistentPreRunE: checkIfAdmin,
345 RunE: func(cmd *cobra.Command, args []string) error {
346 ctx := cmd.Context()
347 be := backend.FromContext(ctx)
348 id, err := strconv.ParseInt(args[1], 10, 64)
349 if err != nil {
350 return fmt.Errorf("invalid webhook ID: %w", err)
351 }
352
353 delID, err := uuid.Parse(args[2])
354 if err != nil {
355 return fmt.Errorf("invalid delivery ID: %w", err)
356 }
357
358 del, err := be.WebhookDelivery(ctx, id, delID)
359 if err != nil {
360 return err //nolint:wrapcheck
361 }
362
363 out := cmd.OutOrStdout()
364 fmt.Fprintf(out, "ID: %s\n", del.ID) //nolint:errcheck
365 fmt.Fprintf(out, "Event: %s\n", del.Event) //nolint:errcheck
366 fmt.Fprintf(out, "Request URL: %s\n", del.RequestURL) //nolint:errcheck
367 fmt.Fprintf(out, "Request Method: %s\n", del.RequestMethod) //nolint:errcheck
368 fmt.Fprintf(out, "Request Error: %s\n", del.RequestError.String) //nolint:errcheck
369 fmt.Fprintf(out, "Request Headers:\n") //nolint:errcheck
370 reqHeaders := strings.Split(del.RequestHeaders, "\n")
371 for _, h := range reqHeaders {
372 fmt.Fprintf(out, " %s\n", h) //nolint:errcheck
373 }
374
375 fmt.Fprintf(out, "Request Body:\n") //nolint:errcheck
376 reqBody := strings.Split(del.RequestBody, "\n")
377 for _, b := range reqBody {
378 fmt.Fprintf(out, " %s\n", b) //nolint:errcheck
379 }
380
381 fmt.Fprintf(out, "Response Status: %d\n", del.ResponseStatus) //nolint:errcheck
382 fmt.Fprintf(out, "Response Headers:\n") //nolint:errcheck
383 resHeaders := strings.Split(del.ResponseHeaders, "\n")
384 for _, h := range resHeaders {
385 fmt.Fprintf(out, " %s\n", h) //nolint:errcheck
386 }
387
388 fmt.Fprintf(out, "Response Body:\n") //nolint:errcheck
389 resBody := strings.Split(del.ResponseBody, "\n")
390 for _, b := range resBody {
391 fmt.Fprintf(out, " %s\n", b) //nolint:errcheck
392 }
393
394 return nil
395 },
396 }
397
398 return cmd
399}