-
Notifications
You must be signed in to change notification settings - Fork 0
feat(control-plane): proxy webhook subscription create/delete to relayfile-cloud #345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,6 +9,7 @@ import ( | |||||||||||||
| "fmt" | ||||||||||||||
| "io" | ||||||||||||||
| "net/http" | ||||||||||||||
| "net/url" | ||||||||||||||
| "os" | ||||||||||||||
| "os/signal" | ||||||||||||||
| "path/filepath" | ||||||||||||||
|
|
@@ -18,9 +19,9 @@ import ( | |||||||||||||
| "time" | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| const relayfileControlPlaneAPIVersion uint32 = 1 | ||||||||||||||
| const relayfileControlPlaneAPIVersion uint32 = 2 | ||||||||||||||
|
|
||||||||||||||
| var relayfileControlPlaneSupportedVersions = []uint32{relayfileControlPlaneAPIVersion} | ||||||||||||||
| var relayfileControlPlaneSupportedVersions = []uint32{1, relayfileControlPlaneAPIVersion} | ||||||||||||||
|
|
||||||||||||||
| type controlPlaneErrorCode string | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -119,6 +120,23 @@ type writebackSecretData struct { | |||||||||||||
| Secret string `json:"secret"` | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| type webhookSubscriptionRequest struct { | ||||||||||||||
| Workspace string `json:"workspace,omitempty"` | ||||||||||||||
| URL string `json:"url"` | ||||||||||||||
| PathGlobs []string `json:"pathGlobs"` | ||||||||||||||
| Secret string `json:"secret"` | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| type webhookSubscriptionResponse struct { | ||||||||||||||
| SubscriptionID string `json:"subscriptionId"` | ||||||||||||||
| Secret string `json:"secret,omitempty"` | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| type deleteWebhookSubscriptionRequest struct { | ||||||||||||||
| Workspace string `json:"workspace,omitempty"` | ||||||||||||||
| SubscriptionID string `json:"subscriptionId"` | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| func runControlPlane(args []string, stdout io.Writer) error { | ||||||||||||||
| if len(args) == 0 { | ||||||||||||||
| return errors.New("control-plane subcommand is required: serve") | ||||||||||||||
|
|
@@ -210,6 +228,7 @@ func newControlPlaneHandler() http.Handler { | |||||||||||||
| mux.HandleFunc("/v1/integrations/bindings", withControlPlaneVersion(handleControlPlaneListBindings)) | ||||||||||||||
| mux.HandleFunc("/v1/integrations/unbind", withControlPlaneVersion(handleControlPlaneUnbind)) | ||||||||||||||
| mux.HandleFunc("/v1/integrations/writeback-secret", withControlPlaneVersion(handleControlPlaneWritebackSecret)) | ||||||||||||||
| mux.HandleFunc("/v1/integrations/webhook-subscriptions", withControlPlaneVersion(handleControlPlaneWebhookSubscription)) | ||||||||||||||
| return mux | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -449,6 +468,79 @@ func handleControlPlaneWritebackSecret(w http.ResponseWriter, r *http.Request) { | |||||||||||||
| writeControlPlaneJSON(w, http.StatusOK, data) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| func handleControlPlaneWebhookSubscription(w http.ResponseWriter, r *http.Request) { | ||||||||||||||
| switch r.Method { | ||||||||||||||
| case http.MethodPost: | ||||||||||||||
| var req webhookSubscriptionRequest | ||||||||||||||
| if err := decodeControlPlaneJSON(r, &req); err != nil { | ||||||||||||||
| writeControlPlaneError(w, http.StatusBadRequest, controlPlaneErrInvalidArgument, err.Error()) | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
| if strings.TrimSpace(req.URL) == "" || len(req.PathGlobs) == 0 || strings.TrimSpace(req.Secret) == "" { | ||||||||||||||
| writeControlPlaneError(w, http.StatusBadRequest, controlPlaneErrInvalidArgument, "url, pathGlobs, and secret are required") | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
| commandClient, err := prepareWorkspaceCommandClient(strings.TrimSpace(req.Workspace), "", "", defaultJoinScopes) | ||||||||||||||
| if err != nil { | ||||||||||||||
| writeControlPlaneMappedError(w, err) | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
| workspaceID := strings.TrimSpace(commandClient.workspaceID) | ||||||||||||||
| if workspaceID == "" { | ||||||||||||||
| writeControlPlaneMappedError(w, errors.New("could not resolve relayfile workspace id")) | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
| var response webhookSubscriptionResponse | ||||||||||||||
| if err := commandClient.client.postJSON( | ||||||||||||||
| r.Context(), | ||||||||||||||
| fmt.Sprintf("/v1/workspaces/%s/webhooks", url.PathEscape(workspaceID)), | ||||||||||||||
|
Comment on lines
+494
to
+496
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in d422ef6 — webhook subscription creation now passes r.Context() through to the upstream relayfile-cloud request. |
||||||||||||||
| map[string]any{ | ||||||||||||||
| "url": strings.TrimSpace(req.URL), | ||||||||||||||
| "pathGlobs": req.PathGlobs, | ||||||||||||||
| "secret": strings.TrimSpace(req.Secret), | ||||||||||||||
| }, | ||||||||||||||
| &response, | ||||||||||||||
| ); err != nil { | ||||||||||||||
| writeControlPlaneMappedError(w, err) | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
| writeControlPlaneJSON(w, http.StatusOK, response) | ||||||||||||||
| case http.MethodDelete: | ||||||||||||||
| var req deleteWebhookSubscriptionRequest | ||||||||||||||
| if err := decodeControlPlaneJSON(r, &req); err != nil { | ||||||||||||||
| writeControlPlaneError(w, http.StatusBadRequest, controlPlaneErrInvalidArgument, err.Error()) | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
| subscriptionID := strings.TrimSpace(req.SubscriptionID) | ||||||||||||||
| if subscriptionID == "" { | ||||||||||||||
| writeControlPlaneError(w, http.StatusBadRequest, controlPlaneErrInvalidArgument, "subscriptionId is required") | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
| commandClient, err := prepareWorkspaceCommandClient(strings.TrimSpace(req.Workspace), "", "", defaultJoinScopes) | ||||||||||||||
| if err != nil { | ||||||||||||||
| writeControlPlaneMappedError(w, err) | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
| workspaceID := strings.TrimSpace(commandClient.workspaceID) | ||||||||||||||
| if workspaceID == "" { | ||||||||||||||
| writeControlPlaneMappedError(w, errors.New("could not resolve relayfile workspace id")) | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
| if err := commandClient.client.deleteJSON( | ||||||||||||||
| r.Context(), | ||||||||||||||
| fmt.Sprintf("/v1/workspaces/%s/webhooks/%s", url.PathEscape(workspaceID), url.PathEscape(subscriptionID)), | ||||||||||||||
|
Comment on lines
+529
to
+531
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use
Suggested change
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in d422ef6 — webhook subscription deletion now passes r.Context() through to the upstream relayfile-cloud request. |
||||||||||||||
| "", | ||||||||||||||
| nil, | ||||||||||||||
| ); err != nil { | ||||||||||||||
| writeControlPlaneMappedError(w, err) | ||||||||||||||
| return | ||||||||||||||
| } | ||||||||||||||
| writeControlPlaneJSON(w, http.StatusOK, map[string]bool{"ok": true}) | ||||||||||||||
| default: | ||||||||||||||
| writeControlPlaneError(w, http.StatusMethodNotAllowed, controlPlaneErrInvalidArgument, "method not allowed") | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| func controlPlaneHello() helloResponse { | ||||||||||||||
| return helloResponse{ | ||||||||||||||
| DaemonVersion: relayfileVersion, | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: The unbind handler (
handleControlPlaneUnbind) only removes the local relay integration binding and does not clean up the corresponding relayfile-cloud webhook subscription. This creates a lifecycle leak: after unbind, the cloud subscription remains active and can continue delivering inbound webhooks even though the local binding no longer exists. You should persist the subscription ID on the binding so that unbind can proxy a DELETE to/v1/workspaces/{workspaceID}/webhooks/{subscriptionID}before (or as part of) removing the local binding.Prompt for AI agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for flagging this. I am not expanding this PR to make unbind proxy cloud subscription deletion: the local unbind path currently only owns local binding removal and does not carry the authenticated cloud workspace/session context needed to delete relayfile-cloud webhooks safely. The binding already stores subscriptionId for that follow-up lifecycle work, but tying teardown into unbind is a cross-repo behavior change tracked separately. This PR stays scoped to exposing create/delete webhook subscription proxy operations and the typed client API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The unbind cleanup point is fair in general, but it’s out of scope for this PR. This change only adds the webhook-subscription proxy operations and typed client API; the unbind teardown is a separate cross-repo behavior change.