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/webhook"
 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
 56			}
 57
 58			webhooks, err := be.ListWebhooks(ctx, repo)
 59			if err != nil {
 60				return err
 61			}
 62
 63			w := new(tabwriter.Writer)
 64			w.Init(cmd.OutOrStdout(), 5, 0, 1, ' ', 0)
 65			fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
 66				"ID", "URL", "Events", "Active", "CreatedAt", "UpdatedAt",
 67			)
 68			for _, h := range webhooks {
 69				events := make([]string, len(h.Events))
 70				for i, e := range h.Events {
 71					events[i] = e.String()
 72				}
 73				fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\n",
 74					h.ID, h.URL, strings.Join(events, ","),
 75					strconv.FormatBool(h.Active),
 76					h.CreatedAt.Format(time.RFC3339),
 77					h.UpdatedAt.Format(time.RFC3339),
 78				)
 79			}
 80			fmt.Fprintln(w)
 81			w.Flush()
 82			return nil
 83		},
 84	}
 85
 86	return cmd
 87}
 88
 89func webhookCreateCommand() *cobra.Command {
 90	var events []string
 91	var secret string
 92	var active bool
 93	var contentType string
 94	cmd := &cobra.Command{
 95		Use:               "create REPOSITORY URL",
 96		Short:             "Create a repository webhook",
 97		Args:              cobra.ExactArgs(2),
 98		PersistentPreRunE: checkIfAdmin,
 99		RunE: func(cmd *cobra.Command, args []string) error {
100			ctx := cmd.Context()
101			be := backend.FromContext(ctx)
102			repo, err := be.Repository(ctx, args[0])
103			if err != nil {
104				return err
105			}
106
107			var evs []webhook.Event
108			for _, e := range events {
109				ev, err := webhook.ParseEvent(e)
110				if err != nil {
111					return fmt.Errorf("invalid event: %w", err)
112				}
113
114				evs = append(evs, ev)
115			}
116
117			var ct webhook.ContentType
118			switch strings.ToLower(strings.TrimSpace(contentType)) {
119			case "json":
120				ct = webhook.ContentTypeJSON
121			case "form":
122				ct = webhook.ContentTypeForm
123			default:
124				return webhook.ErrInvalidContentType
125			}
126
127			return be.CreateWebhook(ctx, repo, strings.TrimSpace(args[1]), 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			for _, d := range dels {
292				cmd.Printf("%s: created by %s at %s, response status [%d]\n",
293					d.ID.String(),
294					d.Event.String(),
295					d.CreatedAt.Format(time.RFC3339),
296					d.ResponseStatus)
297			}
298			return nil
299		},
300	}
301
302	return cmd
303}
304
305func webhookDeliveriesRedeliverCommand() *cobra.Command {
306	cmd := &cobra.Command{
307		Use:               "redeliver REPOSITORY WEBHOOK_ID DELIVERY_ID",
308		Short:             "Redeliver a webhook delivery",
309		PersistentPreRunE: checkIfAdmin,
310		RunE: func(cmd *cobra.Command, args []string) error {
311			ctx := cmd.Context()
312			be := backend.FromContext(ctx)
313			repo, err := be.Repository(ctx, args[0])
314			if err != nil {
315				return err
316			}
317
318			id, err := strconv.ParseInt(args[1], 10, 64)
319			if err != nil {
320				return fmt.Errorf("invalid webhook ID: %w", err)
321			}
322
323			delID, err := uuid.Parse(args[2])
324			if err != nil {
325				return fmt.Errorf("invalid delivery ID: %w", err)
326			}
327
328			return be.RedeliverWebhookDelivery(ctx, repo, id, delID)
329		},
330	}
331
332	return cmd
333}
334
335func webhookDeliveriesGetCommand() *cobra.Command {
336	cmd := &cobra.Command{
337		Use:               "get REPOSITORY WEBHOOK_ID DELIVERY_ID",
338		Short:             "Get a webhook delivery",
339		PersistentPreRunE: checkIfAdmin,
340		RunE: func(cmd *cobra.Command, args []string) error {
341			ctx := cmd.Context()
342			be := backend.FromContext(ctx)
343			id, err := strconv.ParseInt(args[1], 10, 64)
344			if err != nil {
345				return fmt.Errorf("invalid webhook ID: %w", err)
346			}
347
348			delID, err := uuid.Parse(args[2])
349			if err != nil {
350				return fmt.Errorf("invalid delivery ID: %w", err)
351			}
352
353			del, err := be.WebhookDelivery(ctx, id, delID)
354			if err != nil {
355				return err
356			}
357
358			out := cmd.OutOrStdout()
359			fmt.Fprintf(out, "ID: %s\n", del.ID)                             //nolint:errcheck
360			fmt.Fprintf(out, "Event: %s\n", del.Event)                       //nolint:errcheck
361			fmt.Fprintf(out, "Request URL: %s\n", del.RequestURL)            //nolint:errcheck
362			fmt.Fprintf(out, "Request Method: %s\n", del.RequestMethod)      //nolint:errcheck
363			fmt.Fprintf(out, "Request Error: %s\n", del.RequestError.String) //nolint:errcheck
364			fmt.Fprintf(out, "Request Headers:\n")                           //nolint:errcheck
365			reqHeaders := strings.Split(del.RequestHeaders, "\n")
366			for _, h := range reqHeaders {
367				fmt.Fprintf(out, "  %s\n", h) //nolint:errcheck
368			}
369
370			fmt.Fprintf(out, "Request Body:\n") //nolint:errcheck
371			reqBody := strings.Split(del.RequestBody, "\n")
372			for _, b := range reqBody {
373				fmt.Fprintf(out, "  %s\n", b) //nolint:errcheck
374			}
375
376			fmt.Fprintf(out, "Response Status: %d\n", del.ResponseStatus) //nolint:errcheck
377			fmt.Fprintf(out, "Response Headers:\n")                       //nolint:errcheck
378			resHeaders := strings.Split(del.ResponseHeaders, "\n")
379			for _, h := range resHeaders {
380				fmt.Fprintf(out, "  %s\n", h) //nolint:errcheck
381			}
382
383			fmt.Fprintf(out, "Response Body:\n") //nolint:errcheck
384			resBody := strings.Split(del.ResponseBody, "\n")
385			for _, b := range resBody {
386				fmt.Fprintf(out, "  %s\n", b) //nolint:errcheck
387			}
388
389			return nil
390		},
391	}
392
393	return cmd
394}