From a8ac1fa2a18d4f1d011e2f34a0e634c158ffd268 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Sat, 14 Mar 2026 23:35:16 +0100 Subject: [PATCH] Add proboctl webhook commands Add CRUD commands for webhook subscriptions and a command to list webhook events. Signed-off-by: Bryan Frimin --- pkg/cmd/webhook/create/create.go | 146 ++++++++++++++++++++++++ pkg/cmd/webhook/delete/delete.go | 102 +++++++++++++++++ pkg/cmd/webhook/event/event.go | 32 ++++++ pkg/cmd/webhook/event/list/list.go | 177 +++++++++++++++++++++++++++++ pkg/cmd/webhook/list/list.go | 172 ++++++++++++++++++++++++++++ pkg/cmd/webhook/shared/events.go | 33 ++++++ pkg/cmd/webhook/update/update.go | 134 ++++++++++++++++++++++ pkg/cmd/webhook/view/view.go | 131 +++++++++++++++++++++ pkg/cmd/webhook/webhook.go | 42 +++++++ 9 files changed, 969 insertions(+) create mode 100644 pkg/cmd/webhook/create/create.go create mode 100644 pkg/cmd/webhook/delete/delete.go create mode 100644 pkg/cmd/webhook/event/event.go create mode 100644 pkg/cmd/webhook/event/list/list.go create mode 100644 pkg/cmd/webhook/list/list.go create mode 100644 pkg/cmd/webhook/shared/events.go create mode 100644 pkg/cmd/webhook/update/update.go create mode 100644 pkg/cmd/webhook/view/view.go create mode 100644 pkg/cmd/webhook/webhook.go diff --git a/pkg/cmd/webhook/create/create.go b/pkg/cmd/webhook/create/create.go new file mode 100644 index 000000000..510403515 --- /dev/null +++ b/pkg/cmd/webhook/create/create.go @@ -0,0 +1,146 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package create + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/webhook/shared" +) + +const createMutation = ` +mutation($input: CreateWebhookSubscriptionInput!) { + createWebhookSubscription(input: $input) { + webhookSubscriptionEdge { + node { + id + endpointUrl + selectedEvents + } + } + } +} +` + +type createResponse struct { + CreateWebhookSubscription struct { + WebhookSubscriptionEdge struct { + Node struct { + ID string `json:"id"` + EndpointURL string `json:"endpointUrl"` + SelectedEvents []string `json:"selectedEvents"` + } `json:"node"` + } `json:"webhookSubscriptionEdge"` + } `json:"createWebhookSubscription"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagURL string + flagEvents []string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a webhook subscription", + Example: ` # Create a webhook for all meeting events + proboctl webhook create --url https://example.com/webhook --event MEETING_CREATED --event MEETING_UPDATED + + # Create a webhook for all supported events + proboctl webhook create --url https://example.com/webhook --event MEETING_CREATED --event MEETING_UPDATED --event MEETING_DELETED --event VENDOR_CREATED --event VENDOR_UPDATED --event VENDOR_DELETED`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + for _, e := range flagEvents { + valid := false + for _, v := range shared.ValidEvents { + if e == v { + valid = true + break + } + } + if !valid { + return fmt.Errorf("invalid --event value %q: valid values are %s", e, strings.Join(shared.ValidEvents, ", ")) + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + if flagOrg == "" { + flagOrg = hc.Organization + } + + if flagOrg == "" { + return fmt.Errorf("organization is required; pass --org or set a default with 'proboctl auth login'") + } + + input := map[string]any{ + "organizationId": flagOrg, + "endpointUrl": flagURL, + "selectedEvents": flagEvents, + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + w := resp.CreateWebhookSubscription.WebhookSubscriptionEdge.Node + out := f.IOStreams.Out + _, _ = fmt.Fprintf(out, "Created webhook subscription %s\n", w.ID) + _, _ = fmt.Fprintf(out, "Endpoint: %s\n", w.EndpointURL) + _, _ = fmt.Fprintf(out, "Events: %s\n", strings.Join(w.SelectedEvents, ", ")) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagURL, "url", "", "Webhook endpoint URL (required)") + cmd.Flags().StringSliceVar(&flagEvents, "event", nil, "Event types to subscribe to (required, can be repeated)") + + _ = cmd.MarkFlagRequired("url") + _ = cmd.MarkFlagRequired("event") + + return cmd +} diff --git a/pkg/cmd/webhook/delete/delete.go b/pkg/cmd/webhook/delete/delete.go new file mode 100644 index 000000000..fbdab3132 --- /dev/null +++ b/pkg/cmd/webhook/delete/delete.go @@ -0,0 +1,102 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteWebhookSubscriptionInput!) { + deleteWebhookSubscription(input: $input) { + deletedWebhookSubscriptionId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a webhook subscription", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete webhook subscription: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete webhook subscription %s?", args[0])). + Value(&confirmed). + Run() + if err != nil { + return err + } + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + _, err = client.Do( + deleteMutation, + map[string]any{ + "input": map[string]any{ + "webhookSubscriptionId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted webhook subscription %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/webhook/event/event.go b/pkg/cmd/webhook/event/event.go new file mode 100644 index 000000000..80d2fada1 --- /dev/null +++ b/pkg/cmd/webhook/event/event.go @@ -0,0 +1,32 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package event + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/webhook/event/list" +) + +func NewCmdEvent(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "event ", + Short: "Manage webhook events", + } + + cmd.AddCommand(list.NewCmdList(f)) + + return cmd +} diff --git a/pkg/cmd/webhook/event/list/list.go b/pkg/cmd/webhook/event/list/list.go new file mode 100644 index 000000000..439382755 --- /dev/null +++ b/pkg/cmd/webhook/event/list/list.go @@ -0,0 +1,177 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey, $orderBy: WebhookEventOrder) { + node(id: $id) { + __typename + ... on WebhookSubscription { + events(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + status + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type webhookEvent struct { + ID string `json:"id"` + Status string `json:"status"` + CreatedAt string `json:"createdAt"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagLimit int + flagOrderBy string + flagOrderDir string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list ", + Short: "List events for a webhook subscription", + Aliases: []string{"ls"}, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + variables := map[string]any{ + "id": args[0], + } + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + events, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[webhookEvent], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Events api.Connection[webhookEvent] `json:"events"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + if resp.Node == nil { + return nil, fmt.Errorf("webhook subscription %s not found", args[0]) + } + if resp.Node.Typename != "WebhookSubscription" { + return nil, fmt.Errorf("expected WebhookSubscription node, got %s", resp.Node.Typename) + } + return &resp.Node.Events, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + if events == nil { + events = []webhookEvent{} + } + return cmdutil.PrintJSON(f.IOStreams.Out, events) + } + + if len(events) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No webhook events found.") + return nil + } + + rows := make([][]string, 0, len(events)) + for _, e := range events { + rows = append(rows, []string{ + e.ID, + e.Status, + cmdutil.FormatTime(e.CreatedAt), + }) + } + + t := cmdutil.NewTable("ID", "STATUS", "CREATED").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(events) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d events\n", + len(events), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of events to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/webhook/list/list.go b/pkg/cmd/webhook/list/list.go new file mode 100644 index 000000000..195d54a37 --- /dev/null +++ b/pkg/cmd/webhook/list/list.go @@ -0,0 +1,172 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($first: Int, $after: CursorKey, $orderBy: WebhookSubscriptionOrder) { + viewer { + organization { + webhookSubscriptions(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + endpointUrl + selectedEvents + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type webhookSubscription struct { + ID string `json:"id"` + EndpointURL string `json:"endpointUrl"` + SelectedEvents []string `json:"selectedEvents"` + CreatedAt string `json:"createdAt"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagLimit int + flagOrderBy string + flagOrderDir string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List webhook subscriptions", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + variables := map[string]any{} + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + webhooks, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[webhookSubscription], error) { + var resp struct { + Viewer struct { + Organization struct { + WebhookSubscriptions api.Connection[webhookSubscription] `json:"webhookSubscriptions"` + } `json:"organization"` + } `json:"viewer"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + return &resp.Viewer.Organization.WebhookSubscriptions, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + if webhooks == nil { + webhooks = []webhookSubscription{} + } + return cmdutil.PrintJSON(f.IOStreams.Out, webhooks) + } + + if len(webhooks) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No webhook subscriptions found.") + return nil + } + + rows := make([][]string, 0, len(webhooks)) + for _, w := range webhooks { + rows = append(rows, []string{ + w.ID, + w.EndpointURL, + fmt.Sprintf("%d events", len(w.SelectedEvents)), + cmdutil.FormatTime(w.CreatedAt), + }) + } + + t := cmdutil.NewTable("ID", "ENDPOINT", "EVENTS", "CREATED").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(webhooks) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d webhook subscriptions\n", + len(webhooks), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of webhook subscriptions to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/webhook/shared/events.go b/pkg/cmd/webhook/shared/events.go new file mode 100644 index 000000000..4a9dbba1a --- /dev/null +++ b/pkg/cmd/webhook/shared/events.go @@ -0,0 +1,33 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package shared + +// ValidEvents is the set of event types accepted by the webhook +// create and update commands. Keep in sync with +// pkg/coredata/webhook_event_type.go. +var ValidEvents = []string{ + "MEETING_CREATED", + "MEETING_UPDATED", + "MEETING_DELETED", + "VENDOR_CREATED", + "VENDOR_UPDATED", + "VENDOR_DELETED", + "USER_CREATED", + "USER_UPDATED", + "USER_DELETED", + "OBLIGATION_CREATED", + "OBLIGATION_UPDATED", + "OBLIGATION_DELETED", +} diff --git a/pkg/cmd/webhook/update/update.go b/pkg/cmd/webhook/update/update.go new file mode 100644 index 000000000..6e914fb04 --- /dev/null +++ b/pkg/cmd/webhook/update/update.go @@ -0,0 +1,134 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package update + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/webhook/shared" +) + +const updateMutation = ` +mutation($input: UpdateWebhookSubscriptionInput!) { + updateWebhookSubscription(input: $input) { + webhookSubscription { + id + endpointUrl + selectedEvents + } + } +} +` + +type updateResponse struct { + UpdateWebhookSubscription struct { + WebhookSubscription struct { + ID string `json:"id"` + EndpointURL string `json:"endpointUrl"` + SelectedEvents []string `json:"selectedEvents"` + } `json:"webhookSubscription"` + } `json:"updateWebhookSubscription"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagURL string + flagEvents []string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a webhook subscription", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + input := map[string]any{ + "id": args[0], + } + + if cmd.Flags().Changed("url") { + input["endpointUrl"] = flagURL + } + + if cmd.Flags().Changed("event") { + for _, e := range flagEvents { + valid := false + for _, v := range shared.ValidEvents { + if e == v { + valid = true + break + } + } + if !valid { + return fmt.Errorf("invalid --event value %q: valid values are %s", e, strings.Join(shared.ValidEvents, ", ")) + } + } + input["selectedEvents"] = flagEvents + } + + if len(input) == 1 { + return fmt.Errorf("at least one of --url or --event must be specified") + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + data, err := client.Do( + updateMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + w := resp.UpdateWebhookSubscription.WebhookSubscription + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated webhook subscription %s\n", + w.ID, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagURL, "url", "", "Webhook endpoint URL") + cmd.Flags().StringSliceVar(&flagEvents, "event", nil, "Event types to subscribe to (replaces existing)") + + return cmd +} diff --git a/pkg/cmd/webhook/view/view.go b/pkg/cmd/webhook/view/view.go new file mode 100644 index 000000000..c8715565e --- /dev/null +++ b/pkg/cmd/webhook/view/view.go @@ -0,0 +1,131 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on WebhookSubscription { + id + endpointUrl + selectedEvents + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + EndpointURL string `json:"endpointUrl"` + SelectedEvents []string `json:"selectedEvents"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a webhook subscription", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("webhook subscription %s not found", args[0]) + } + + if resp.Node.Typename != "WebhookSubscription" { + return fmt.Errorf("expected WebhookSubscription node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + w := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render("Webhook Subscription")) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), w.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Endpoint:"), w.EndpointURL) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Events:"), strings.Join(w.SelectedEvents, ", ")) + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(w.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(w.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/webhook/webhook.go b/pkg/cmd/webhook/webhook.go new file mode 100644 index 000000000..32103d70f --- /dev/null +++ b/pkg/cmd/webhook/webhook.go @@ -0,0 +1,42 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package webhook + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/webhook/create" + "go.probo.inc/probo/pkg/cmd/webhook/delete" + "go.probo.inc/probo/pkg/cmd/webhook/event" + "go.probo.inc/probo/pkg/cmd/webhook/list" + "go.probo.inc/probo/pkg/cmd/webhook/update" + "go.probo.inc/probo/pkg/cmd/webhook/view" +) + +func NewCmdWebhook(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "webhook ", + Short: "Manage webhook subscriptions", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + cmd.AddCommand(event.NewCmdEvent(f)) + + return cmd +}