Skip to content

feat(reconcile): add a Webhook kind#1772

Open
rohilsurana wants to merge 14 commits into
mainfrom
feat/reconcile-webhook-kind
Open

feat(reconcile): add a Webhook kind#1772
rohilsurana wants to merge 14 commits into
mainfrom
feat/reconcile-webhook-kind

Conversation

@rohilsurana

@rohilsurana rohilsurana commented Jul 17, 2026

Copy link
Copy Markdown
Member

What

Adds a Webhook kind to the reconcile flow, so webhook endpoints are managed from a
desired-state file like platform users, permissions, roles, and preferences. Also adds the
server-side validation that lets the kind round-trip cleanly.

How the kind behaves

Follows the rules in RFC 0001. A webhook is an object keyed by its URL.

  • The URL is the identity and never changes. A missing endpoint fails the plan; deleting one
    needs delete: true.
  • Managed fields: description, subscribed events, and state. subscribed_events is the full
    desired set: an empty or omitted list means all events (the server default), and it is
    deduplicated and compared as a set. description and state are managed the ordinary way.
  • The signing secret is server-owned. The server generates it on create and never returns it
    on read, so it is never in the file, a plan, or an export.

Server-side validation (closes the round-trip gap)

The reconcile flow treats the URL as an endpoint's identity and manages state as
enabled/disabled. For export to round-trip over every reachable server state, the server must
not hold values the reconciler cannot represent. So the webhook service now validates on create
and update:

  • the URL is a valid absolute URL,
  • the state is enabled or disabled (or empty, which defaults to enabled),
  • the URL is unique across endpoints (it is the identity).

Invalid input now returns InvalidArgument, and a duplicate URL returns AlreadyExists, instead
of the previous generic internal error. Uniqueness is enforced in the service; a DB unique index
on webhook_endpoints.url would be a stronger guarantee if maintainers prefer, and either way a
server that already holds duplicate or malformed rows from before this change would need a
one-time cleanup.

Changes

  • internal/reconcile/webhook.go, webhook_reconciler.go: the kind, its diff, and export.
  • cmd/reconcile.go: register the kind and update the help text.
  • core/webhook/service.go: validate URL and state, enforce URL uniqueness, with a service test.
  • internal/api/v1beta1connect/webhook.go: map validation errors to InvalidArgument /
    AlreadyExists.
  • Docs: a Webhook section in the reconcile guide and the CLI reference kinds list.

Testing

  • go test ./internal/reconcile/... ./core/webhook/... passes, including the diff, apply,
    export round-trip, dedup, and the new server-side validation cases.
  • go build, go vet, gofmt, and golangci-lint are clean.

Follow-up

When the Validate() interface change (#1776) merges, this kind needs the same
per-document Validate() method to satisfy the interface. It is a small method mirroring the
other kinds, added when this branch rebases on that change.

@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Jul 21, 2026 5:41am

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added Webhook to export and reconcile workflows, including desired-state management, planning/apply (with dry-run), and exporting current webhook configuration.
    • Reconciliation help/behavior now clarifies that deletion/reset requires an explicit delete: true, and omitted settings follow the documented defaults.
  • Bug Fixes / Improvements

    • Webhook endpoint URLs are now trimmed and strictly validated as absolute http/https URLs with clear conflict detection for duplicate URLs.
    • API error responses for webhook operations are now reported with more precise status codes.
  • Tests

    • Added coverage for validation, uniqueness, diffing, reconciliation behavior, and exporter round-tripping.
  • Documentation

    • Updated reconcile and CLI reference docs for webhook kind and deletion semantics.

Walkthrough

Adds webhook endpoint validation and uniqueness checks, maps webhook service errors to API status codes, introduces webhook desired-state reconciliation and export support, registers the reconciler with the CLI, and documents webhook reconciliation semantics.

Changes

Webhook endpoint handling

Layer / File(s) Summary
Endpoint validation and API errors
core/webhook/service.go, core/webhook/service_test.go, internal/api/v1beta1connect/webhook.go
Endpoint URLs and states are validated before persistence, duplicate URLs are rejected, and webhook service errors map to Connect status codes.

Webhook reconciliation

Layer / File(s) Summary
Webhook desired-state planning
internal/reconcile/role.go, internal/reconcile/webhook.go, internal/reconcile/webhook_test.go
Webhook specs, current-state models, normalization, validation, and ordered add/update/remove planning are implemented and tested.
Reconciliation and export execution
internal/reconcile/webhook_reconciler.go, internal/reconcile/webhook_reconciler_test.go
Webhook list, create, update, delete, dry-run, metadata preservation, and export round-trip behavior are implemented and tested.
CLI registration and documentation
cmd/reconcile.go, docs/content/docs/reconcile.mdx, docs/content/docs/reference/cli.mdx
The webhook reconciler is registered and webhook export, deletion, omission, and desired-state rules are documented.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: whoabhisheksah

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coveralls

coveralls commented Jul 17, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 29804701004

Coverage increased (+0.4%) to 46.587%

Details

  • Coverage increased (+0.4%) from the base build.
  • Patch coverage: 40 uncovered changes across 5 files (272 of 312 lines covered, 87.18%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
internal/reconcile/webhook_reconciler.go 113 94 83.19%
internal/api/v1beta1connect/webhook.go 14 0 0.0%
core/webhook/service.go 37 33 89.19%
internal/reconcile/webhook.go 127 125 98.43%
cmd/reconcile.go 7 6 85.71%
Total (6 files) 312 272 87.18%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
cmd/reconcile.go 1 43.53%

Coverage Stats

Coverage Status
Relevant Lines: 38738
Covered Lines: 18047
Line Coverage: 46.59%
Coverage Strength: 13.45 hits per line

💛 - Coveralls

@rohilsurana
rohilsurana force-pushed the feat/reconcile-webhook-kind branch from 71b308b to 45fd480 Compare July 20, 2026 17:27
@rohilsurana
rohilsurana marked this pull request as ready for review July 20, 2026 17:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
internal/api/v1beta1connect/webhook.go (1)

15-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

webhookErrCode isn't applied to ListWebhooks/DeleteWebhook.

CreateWebhook and UpdateWebhook now surface proper status codes via webhookErrCode, but ListWebhooks (line 88) and DeleteWebhook (line 108) still hardcode connect.CodeInternal. A delete of a nonexistent webhook would plausibly return webhook.ErrNotFound from the service and should map to CodeNotFound, matching this handler's own new convention.

♻️ Apply consistently
 	err := h.webhookService.DeleteEndpoint(ctx, webhookID)
 	if err != nil {
-		return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("DeleteWebhook: webhook_id=%s: %w", webhookID, err))
+		return nil, connect.NewError(webhookErrCode(err), fmt.Errorf("DeleteWebhook: webhook_id=%s: %w", webhookID, err))
 	}
internal/reconcile/webhook.go (1)

81-95: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Extract a shared normalize+validate+dedupe step; also trim URLs to match server normalization. validateWebhookSpec treats a whitespace-only URL as empty but then parses/stores/compares the raw untrimmed value, while core/webhook/service.go's CreateEndpoint/UpdateEndpoint always strings.TrimSpace the URL before validating/storing. A spec URL with incidental whitespace can pass client-side validation yet fail to match the server's (trimmed) URL in diffWebhooks's byURL lookup, producing a spurious "add" that the server then rejects as a duplicate. Separately, the duplicate-URL "seen"-map dedupe loop is written independently in both diffWebhooks and Validate.

  • internal/reconcile/webhook.go#L81-L95: trim s.URL (e.g. s.URL = strings.TrimSpace(s.URL)) before parsing/validating, and use the trimmed value for storage/comparison in diffWebhooks.
  • internal/reconcile/webhook_reconciler.go#L37-L53: replace the standalone validate+seen-map loop with a shared helper (e.g. normalizeAndValidateWebhookSpecs([]WebhookSpec) ([]WebhookSpec, error)) reused by diffWebhooks, so both call sites trim/validate/dedupe identically.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5cbe9a70-6de0-457b-bfd2-93c50daea78f

📥 Commits

Reviewing files that changed from the base of the PR and between 9c2a0b0 and 45fd480.

📒 Files selected for processing (11)
  • cmd/reconcile.go
  • core/webhook/service.go
  • core/webhook/service_test.go
  • docs/content/docs/reconcile.mdx
  • docs/content/docs/reference/cli.mdx
  • internal/api/v1beta1connect/webhook.go
  • internal/reconcile/role.go
  • internal/reconcile/webhook.go
  • internal/reconcile/webhook_reconciler.go
  • internal/reconcile/webhook_reconciler_test.go
  • internal/reconcile/webhook_test.go

Comment thread core/webhook/service.go
Comment on lines +53 to +59
endpoint.URL = strings.TrimSpace(endpoint.URL)
if err := validateEndpoint(endpoint); err != nil {
return Endpoint{}, err
}
if err := s.ensureURLIsFree(ctx, endpoint.URL, endpoint.ID); err != nil {
return Endpoint{}, err
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

TOCTOU race on URL uniqueness, plus a full table scan on every write.

ensureURLIsFree lists all endpoints and checks in Go, then Create/UpdateByID runs as a separate, unsynchronized step. Two concurrent CreateEndpoint calls for the same URL can both pass the check and both persist — exactly the "two endpoints sharing a url" scenario that internal/reconcile/webhook.go's diffWebhooks has to special-case as an unrecoverable error ("the url identity is ambiguous... delete the extra one by hand"). This check-then-act pattern is unreliable without a DB-level unique constraint or a lock/transaction spanning the check and the write.

Separately, s.eRepo.List(ctx, EndpointFilter{}) fetches every endpoint on every create/update, which won't scale as the number of webhooks grows.

Recommend enforcing uniqueness with a unique index at the storage layer (with the app-level check kept as a fast-path/better error message), and, if a targeted lookup by URL is available on the repository, using that instead of a full list scan.

Also applies to: 77-83, 108-123

Comment thread core/webhook/service.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/webhook/service.go (1)

53-59: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Canonicalize URLs before enforcing identity uniqueness.

Only surrounding whitespace is normalized here, while ensureURLIsFree compares strings byte-for-byte. Equivalent targets such as https://EXAMPLE.com and https://example.com/ can therefore create multiple records for one webhook destination. Parse and canonicalize the URL once, persist that canonical form, and compare canonical values.

Also applies to: 77-83


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e3aa9d73-003c-438e-8cbc-29f324acdb9c

📥 Commits

Reviewing files that changed from the base of the PR and between 45fd480 and d2832dc.

📒 Files selected for processing (6)
  • core/webhook/service.go
  • core/webhook/service_test.go
  • internal/api/v1beta1connect/webhook.go
  • internal/reconcile/webhook.go
  • internal/reconcile/webhook_reconciler.go
  • internal/reconcile/webhook_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • core/webhook/service_test.go
  • internal/reconcile/webhook.go
  • internal/reconcile/webhook_reconciler.go
  • internal/reconcile/webhook_test.go

Comment thread core/webhook/service.go
Comment on lines +96 to +105
func validateEndpoint(endpoint Endpoint) error {
u, err := url.Parse(endpoint.URL)
if err != nil || !u.IsAbs() {
return fmt.Errorf("%w: url must be a valid absolute URL", ErrInvalidDetail)
}
// The server dispatches events to this URL, so restrict it to http(s). This
// keeps other schemes (file, gopher, ...) out of the delivery path.
if u.Scheme != "http" && u.Scheme != "https" {
return fmt.Errorf("%w: url scheme must be http or https", ErrInvalidDetail)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map relevant files and symbols first
git ls-files core/webhook
printf '\n--- outline: core/webhook/service.go ---\n'
ast-grep outline core/webhook/service.go --view expanded || true

printf '\n--- outline: any tests in core/webhook ---\n'
fd -a -t f . core/webhook | sed 's#^\./##' | sort

Repository: raystack/frontier

Length of output: 1864


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the webhook service implementation around validation and uniqueness.
sed -n '1,220p' core/webhook/service.go | cat -n

printf '\n--- search for URL uniqueness and trimming ---\n'
rg -n "ensureURLIsFree|validateEndpoint|TrimSpace|Parse\\(" core/webhook/service.go core/webhook -g '!**/*_test.go'

Repository: raystack/frontier

Length of output: 10217


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect tests that may cover webhook URL validation/uniqueness.
fd -a -t f '.*_test\\.go' core/webhook | sort
printf '\n--- relevant test snippets ---\n'
rg -n "http:foo|http:///|IsAbs|Host|ensureURLIsFree|duplicate|TrimSpace" core/webhook -g '*_test.go'

Repository: raystack/frontier

Length of output: 187


Require a host for HTTP(S) webhook URLs.

url.Parse + IsAbs() still accepts values like http:foo or http:///path, so endpoints without a destination host can be saved and only fail during delivery. Reject empty u.Host here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants