soft-serve

Fork https://github.com/charmbracelet/soft-serve

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

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