soft-serve

git clone git://git.lin.moe/fork/soft-serve.git

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