1package cmd23import (4 "fmt"5 "strconv"6 "strings"7 "text/tabwriter"8 "time"910 "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)1516func webhookCommand() *cobra.Command {17 cmd := &cobra.Command{18 Use: "webhook",19 Aliases: []string{"webhooks"},20 Short: "Manage repository webhooks",21 }2223 cmd.AddCommand(24 webhookListCommand(),25 webhookCreateCommand(),26 webhookDeleteCommand(),27 webhookUpdateCommand(),28 webhookDeliveriesCommand(),29 )3031 return cmd32}3334var webhookEvents []string3536func init() {37 events := webhook.Events()38 webhookEvents = make([]string, len(events))39 for i, e := range events {40 webhookEvents[i] = e.String()41 }42}4344func 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 err56 }5758 webhooks, err := be.ListWebhooks(ctx, repo)59 if err != nil {60 return err61 }6263 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 nil83 },84 }8586 return cmd87}8889func webhookCreateCommand() *cobra.Command {90 var events []string91 var secret string92 var active bool93 var contentType string94 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 err105 }106107 var evs []webhook.Event108 for _, e := range events {109 ev, err := webhook.ParseEvent(e)110 if err != nil {111 return fmt.Errorf("invalid event: %w", err)112 }113114 evs = append(evs, ev)115 }116117 var ct webhook.ContentType118 switch strings.ToLower(strings.TrimSpace(contentType)) {119 case "json":120 ct = webhook.ContentTypeJSON121 case "form":122 ct = webhook.ContentTypeForm123 default:124 return webhook.ErrInvalidContentType125 }126127 return be.CreateWebhook(ctx, repo, strings.TrimSpace(args[1]), ct, secret, evs, active)128 },129 }130131 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`")135136 return cmd137}138139func 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 err151 }152153 id, err := strconv.ParseInt(args[1], 10, 64)154 if err != nil {155 return fmt.Errorf("invalid webhook ID: %w", err)156 }157158 return be.DeleteWebhook(ctx, repo, id)159 },160 }161162 return cmd163}164165func webhookUpdateCommand() *cobra.Command {166 var events []string167 var secret string168 var active string169 var contentType string170 var url string171 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 err182 }183184 id, err := strconv.ParseInt(args[1], 10, 64)185 if err != nil {186 return fmt.Errorf("invalid webhook ID: %w", err)187 }188189 wh, err := be.Webhook(ctx, repo, id)190 if err != nil {191 return err192 }193194 newURL := wh.URL195 if url != "" {196 newURL = url197 }198199 newSecret := wh.Secret200 if secret != "" {201 newSecret = secret202 }203204 newActive := wh.Active205 if active != "" {206 active, err := strconv.ParseBool(active)207 if err != nil {208 return fmt.Errorf("invalid active value: %w", err)209 }210211 newActive = active212 }213214 newContentType := wh.ContentType215 if contentType != "" {216 var ct webhook.ContentType217 switch strings.ToLower(strings.TrimSpace(contentType)) {218 case "json":219 ct = webhook.ContentTypeJSON220 case "form":221 ct = webhook.ContentTypeForm222 default:223 return webhook.ErrInvalidContentType224 }225 newContentType = ct226 }227228 newEvents := wh.Events229 if len(events) > 0 {230 var evs []webhook.Event231 for _, e := range events {232 ev, err := webhook.ParseEvent(e)233 if err != nil {234 return fmt.Errorf("invalid event: %w", err)235 }236237 evs = append(evs, ev)238 }239240 newEvents = evs241 }242243 return be.UpdateWebhook(ctx, repo, id, newURL, newContentType, newSecret, newEvents, newActive)244 },245 }246247 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")252253 return cmd254}255256func webhookDeliveriesCommand() *cobra.Command {257 cmd := &cobra.Command{258 Use: "deliveries",259 Short: "Manage webhook deliveries",260 Aliases: []string{"delivery", "deliver"},261 }262263 cmd.AddCommand(264 webhookDeliveriesListCommand(),265 webhookDeliveriesRedeliverCommand(),266 webhookDeliveriesGetCommand(),267 )268269 return cmd270}271272func 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 }285286 dels, err := be.ListWebhookDeliveries(ctx, id)287 if err != nil {288 return err289 }290291 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 nil299 },300 }301302 return cmd303}304305func 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 err316 }317318 id, err := strconv.ParseInt(args[1], 10, 64)319 if err != nil {320 return fmt.Errorf("invalid webhook ID: %w", err)321 }322323 delID, err := uuid.Parse(args[2])324 if err != nil {325 return fmt.Errorf("invalid delivery ID: %w", err)326 }327328 return be.RedeliverWebhookDelivery(ctx, repo, id, delID)329 },330 }331332 return cmd333}334335func 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 }347348 delID, err := uuid.Parse(args[2])349 if err != nil {350 return fmt.Errorf("invalid delivery ID: %w", err)351 }352353 del, err := be.WebhookDelivery(ctx, id, delID)354 if err != nil {355 return err356 }357358 out := cmd.OutOrStdout()359 fmt.Fprintf(out, "ID: %s\n", del.ID) //nolint:errcheck360 fmt.Fprintf(out, "Event: %s\n", del.Event) //nolint:errcheck361 fmt.Fprintf(out, "Request URL: %s\n", del.RequestURL) //nolint:errcheck362 fmt.Fprintf(out, "Request Method: %s\n", del.RequestMethod) //nolint:errcheck363 fmt.Fprintf(out, "Request Error: %s\n", del.RequestError.String) //nolint:errcheck364 fmt.Fprintf(out, "Request Headers:\n") //nolint:errcheck365 reqHeaders := strings.Split(del.RequestHeaders, "\n")366 for _, h := range reqHeaders {367 fmt.Fprintf(out, " %s\n", h) //nolint:errcheck368 }369370 fmt.Fprintf(out, "Request Body:\n") //nolint:errcheck371 reqBody := strings.Split(del.RequestBody, "\n")372 for _, b := range reqBody {373 fmt.Fprintf(out, " %s\n", b) //nolint:errcheck374 }375376 fmt.Fprintf(out, "Response Status: %d\n", del.ResponseStatus) //nolint:errcheck377 fmt.Fprintf(out, "Response Headers:\n") //nolint:errcheck378 resHeaders := strings.Split(del.ResponseHeaders, "\n")379 for _, h := range resHeaders {380 fmt.Fprintf(out, " %s\n", h) //nolint:errcheck381 }382383 fmt.Fprintf(out, "Response Body:\n") //nolint:errcheck384 resBody := strings.Split(del.ResponseBody, "\n")385 for _, b := range resBody {386 fmt.Fprintf(out, " %s\n", b) //nolint:errcheck387 }388389 return nil390 },391 }392393 return cmd394}