1package cmd23import (4 "fmt"5 "strconv"6 "strings"78 "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)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 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 }7071 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 nil82 },83 }8485 return cmd86}8788func webhookCreateCommand() *cobra.Command {89 var events []string90 var secret string91 var active bool92 var contentType string93 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 err104 }105106 var evs []webhook.Event107 for _, e := range events {108 ev, err := webhook.ParseEvent(e)109 if err != nil {110 return fmt.Errorf("invalid event: %w", err)111 }112113 evs = append(evs, ev)114 }115116 var ct webhook.ContentType117 switch strings.ToLower(strings.TrimSpace(contentType)) {118 case "json":119 ct = webhook.ContentTypeJSON120 case "form":121 ct = webhook.ContentTypeForm122 default:123 return webhook.ErrInvalidContentType124 }125126 url := utils.Sanitize(args[1])127 return be.CreateWebhook(ctx, repo, strings.TrimSpace(url), 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 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 nil306 },307 }308309 return cmd310}311312func 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 err324 }325326 id, err := strconv.ParseInt(args[1], 10, 64)327 if err != nil {328 return fmt.Errorf("invalid webhook ID: %w", err)329 }330331 delID, err := uuid.Parse(args[2])332 if err != nil {333 return fmt.Errorf("invalid delivery ID: %w", err)334 }335336 return be.RedeliverWebhookDelivery(ctx, repo, id, delID)337 },338 }339340 return cmd341}342343func 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 }356357 delID, err := uuid.Parse(args[2])358 if err != nil {359 return fmt.Errorf("invalid delivery ID: %w", err)360 }361362 del, err := be.WebhookDelivery(ctx, id, delID)363 if err != nil {364 return err365 }366367 out := cmd.OutOrStdout()368 fmt.Fprintf(out, "ID: %s\n", del.ID) //nolint:errcheck369 fmt.Fprintf(out, "Event: %s\n", del.Event) //nolint:errcheck370 fmt.Fprintf(out, "Request URL: %s\n", del.RequestURL) //nolint:errcheck371 fmt.Fprintf(out, "Request Method: %s\n", del.RequestMethod) //nolint:errcheck372 fmt.Fprintf(out, "Request Error: %s\n", del.RequestError.String) //nolint:errcheck373 fmt.Fprintf(out, "Request Headers:\n") //nolint:errcheck374 reqHeaders := strings.Split(del.RequestHeaders, "\n")375 for _, h := range reqHeaders {376 fmt.Fprintf(out, " %s\n", h) //nolint:errcheck377 }378379 fmt.Fprintf(out, "Request Body:\n") //nolint:errcheck380 reqBody := strings.Split(del.RequestBody, "\n")381 for _, b := range reqBody {382 fmt.Fprintf(out, " %s\n", b) //nolint:errcheck383 }384385 fmt.Fprintf(out, "Response Status: %d\n", del.ResponseStatus) //nolint:errcheck386 fmt.Fprintf(out, "Response Headers:\n") //nolint:errcheck387 resHeaders := strings.Split(del.ResponseHeaders, "\n")388 for _, h := range resHeaders {389 fmt.Fprintf(out, " %s\n", h) //nolint:errcheck390 }391392 fmt.Fprintf(out, "Response Body:\n") //nolint:errcheck393 resBody := strings.Split(del.ResponseBody, "\n")394 for _, b := range resBody {395 fmt.Fprintf(out, " %s\n", b) //nolint:errcheck396 }397398 return nil399 },400 }401402 return cmd403}