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/utils"12 "github.com/charmbracelet/soft-serve/pkg/webhook"13 "github.com/google/uuid"14 "github.com/spf13/cobra"15)1617func webhookCommand() *cobra.Command {18 cmd := &cobra.Command{19 Use: "webhook",20 Aliases: []string{"webhooks"},21 Short: "Manage repository webhooks",22 }2324 cmd.AddCommand(25 webhookListCommand(),26 webhookCreateCommand(),27 webhookDeleteCommand(),28 webhookUpdateCommand(),29 webhookDeliveriesCommand(),30 )3132 return cmd33}3435var webhookEvents []string3637func init() {38 events := webhook.Events()39 webhookEvents = make([]string, len(events))40 for i, e := range events {41 webhookEvents[i] = e.String()42 }43}4445func 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 err57 }5859 webhooks, err := be.ListWebhooks(ctx, repo)60 if err != nil {61 return err62 }6364 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 nil84 },85 }8687 return cmd88}8990func webhookCreateCommand() *cobra.Command {91 var events []string92 var secret string93 var active bool94 var contentType string95 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 err106 }107108 var evs []webhook.Event109 for _, e := range events {110 ev, err := webhook.ParseEvent(e)111 if err != nil {112 return fmt.Errorf("invalid event: %w", err)113 }114115 evs = append(evs, ev)116 }117118 var ct webhook.ContentType119 switch strings.ToLower(strings.TrimSpace(contentType)) {120 case "json":121 ct = webhook.ContentTypeJSON122 case "form":123 ct = webhook.ContentTypeForm124 default:125 return webhook.ErrInvalidContentType126 }127128 url := utils.Sanitize(args[1])129 return be.CreateWebhook(ctx, repo, strings.TrimSpace(url), ct, secret, evs, active)130 },131 }132133 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`")137138 return cmd139}140141func 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 err153 }154155 id, err := strconv.ParseInt(args[1], 10, 64)156 if err != nil {157 return fmt.Errorf("invalid webhook ID: %w", err)158 }159160 return be.DeleteWebhook(ctx, repo, id)161 },162 }163164 return cmd165}166167func webhookUpdateCommand() *cobra.Command {168 var events []string169 var secret string170 var active string171 var contentType string172 var url string173 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 err184 }185186 id, err := strconv.ParseInt(args[1], 10, 64)187 if err != nil {188 return fmt.Errorf("invalid webhook ID: %w", err)189 }190191 wh, err := be.Webhook(ctx, repo, id)192 if err != nil {193 return err194 }195196 newURL := wh.URL197 if url != "" {198 newURL = url199 }200201 newSecret := wh.Secret202 if secret != "" {203 newSecret = secret204 }205206 newActive := wh.Active207 if active != "" {208 active, err := strconv.ParseBool(active)209 if err != nil {210 return fmt.Errorf("invalid active value: %w", err)211 }212213 newActive = active214 }215216 newContentType := wh.ContentType217 if contentType != "" {218 var ct webhook.ContentType219 switch strings.ToLower(strings.TrimSpace(contentType)) {220 case "json":221 ct = webhook.ContentTypeJSON222 case "form":223 ct = webhook.ContentTypeForm224 default:225 return webhook.ErrInvalidContentType226 }227 newContentType = ct228 }229230 newEvents := wh.Events231 if len(events) > 0 {232 var evs []webhook.Event233 for _, e := range events {234 ev, err := webhook.ParseEvent(e)235 if err != nil {236 return fmt.Errorf("invalid event: %w", err)237 }238239 evs = append(evs, ev)240 }241242 newEvents = evs243 }244245 return be.UpdateWebhook(ctx, repo, id, newURL, newContentType, newSecret, newEvents, newActive)246 },247 }248249 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")254255 return cmd256}257258func webhookDeliveriesCommand() *cobra.Command {259 cmd := &cobra.Command{260 Use: "deliveries",261 Short: "Manage webhook deliveries",262 Aliases: []string{"delivery", "deliver"},263 }264265 cmd.AddCommand(266 webhookDeliveriesListCommand(),267 webhookDeliveriesRedeliverCommand(),268 webhookDeliveriesGetCommand(),269 )270271 return cmd272}273274func 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 }287288 dels, err := be.ListWebhookDeliveries(ctx, id)289 if err != nil {290 return err291 }292293 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 nil301 },302 }303304 return cmd305}306307func 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 err318 }319320 id, err := strconv.ParseInt(args[1], 10, 64)321 if err != nil {322 return fmt.Errorf("invalid webhook ID: %w", err)323 }324325 delID, err := uuid.Parse(args[2])326 if err != nil {327 return fmt.Errorf("invalid delivery ID: %w", err)328 }329330 return be.RedeliverWebhookDelivery(ctx, repo, id, delID)331 },332 }333334 return cmd335}336337func 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 }349350 delID, err := uuid.Parse(args[2])351 if err != nil {352 return fmt.Errorf("invalid delivery ID: %w", err)353 }354355 del, err := be.WebhookDelivery(ctx, id, delID)356 if err != nil {357 return err358 }359360 out := cmd.OutOrStdout()361 fmt.Fprintf(out, "ID: %s\n", del.ID) //nolint:errcheck362 fmt.Fprintf(out, "Event: %s\n", del.Event) //nolint:errcheck363 fmt.Fprintf(out, "Request URL: %s\n", del.RequestURL) //nolint:errcheck364 fmt.Fprintf(out, "Request Method: %s\n", del.RequestMethod) //nolint:errcheck365 fmt.Fprintf(out, "Request Error: %s\n", del.RequestError.String) //nolint:errcheck366 fmt.Fprintf(out, "Request Headers:\n") //nolint:errcheck367 reqHeaders := strings.Split(del.RequestHeaders, "\n")368 for _, h := range reqHeaders {369 fmt.Fprintf(out, " %s\n", h) //nolint:errcheck370 }371372 fmt.Fprintf(out, "Request Body:\n") //nolint:errcheck373 reqBody := strings.Split(del.RequestBody, "\n")374 for _, b := range reqBody {375 fmt.Fprintf(out, " %s\n", b) //nolint:errcheck376 }377378 fmt.Fprintf(out, "Response Status: %d\n", del.ResponseStatus) //nolint:errcheck379 fmt.Fprintf(out, "Response Headers:\n") //nolint:errcheck380 resHeaders := strings.Split(del.ResponseHeaders, "\n")381 for _, h := range resHeaders {382 fmt.Fprintf(out, " %s\n", h) //nolint:errcheck383 }384385 fmt.Fprintf(out, "Response Body:\n") //nolint:errcheck386 resBody := strings.Split(del.ResponseBody, "\n")387 for _, b := range resBody {388 fmt.Fprintf(out, " %s\n", b) //nolint:errcheck389 }390391 return nil392 },393 }394395 return cmd396}