Skip to content

Control plane: web console, tenant-scoped admin, and fleet config versioning#8

Merged
CMGS merged 19 commits into
mainfrom
feat/control-plane-web
Jul 18, 2026
Merged

Control plane: web console, tenant-scoped admin, and fleet config versioning#8
CMGS merged 19 commits into
mainfrom
feat/control-plane-web

Conversation

@CMGS

@CMGS CMGS commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Delivers a browser-facing control plane for the gateway fleet plus the gateway-side admin surface it needs.

Gateway (Rust)

  • New admin endpoints: GET /admin/config, POST /admin/config/validate, GET /admin/config/versions, POST /admin/config/versions/{id}/rollback, GET /admin/usage/series, and ?ak= exact key lookup.
  • Config publishes serialize on a Postgres advisory lock; ?expected_version= is checked under the lock and answers 409 on a moved head. Retention pruning rides in the same transaction.
  • Tenant scope hardening: vendor_cost_micros is redacted for tenant tokens on all three usage reads (policy centralized in AdminScope::sees_vendor_cost); ?ak= lookups answer an empty page for foreign keys instead of a 404 oracle.
  • Allocation reductions on the request path: response fields moved out of parsed JSON instead of cloned, bytes::Bytes response bodies, SSE chunks borrow id/model, account selection without per-call name clones.

Control plane (Go + React)

  • Go BFF: argon2id login with throttle and timing decoy, opaque sessions (memory/Redis), CSRF, password-reset session eviction, three roles (member / tenant admin / system admin), X-Request-ID threaded through to gateway calls.
  • Key mutations authenticate with per-tenant gateway tokens (CP_GATEWAY_TENANT_TOKENS) and fail closed when a tenant has no token.
  • React console: overview, usage & cost, availability (per-instance account pools), keys, users, config publish/rollback, audit — role-gated.
  • OpenAPI spec kept in sync by a drift test against the route table.

Delivery

  • CI builds and E2Es both components; release workflow ships control-plane binaries + web assets; Docker images for gateway and control plane.

Verification

  • cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace (383 passed)
  • go vet, go test ./...; web: vitest + tsc -b build; Playwright e2e in CI
  • make test-integration: real Postgres/Redis via compose, tenant-token full-chain E2E against a real gateway binary (cross-tenant probes answer 404, mutations without a tenant token fail closed with 502, tenant tokens read vendor cost as 0 on real billed traffic), guarded-publish contention test admits exactly one of eight concurrent publishers
  • Two-node fleet + control plane brought up locally from the built images; publish/rollback and scoped views exercised against live traffic

CMGS added 19 commits July 18, 2026 16:28
Security: tenant-scoped key create rejects takeover of another tenant's
AK; demo seed accounts now require explicit CP_DEV_SEED=true; login
throttling plus a timing-safe verify for unknown emails; constant-time
CSRF compare; password reset evicts live sessions (session issue epoch
vs password change epoch); admin mutations get actor-attributed audit
logs; requireAdmin is an allowlist.

Config: rollback re-validates the retained document before republishing
(a stale doc would brick peer reloads and fresh boots); publish supports
expected_version compare-and-insert with a 409 on a moved head, and the
UI confirms and sends its loaded version. /admin/usage/series is one
bucketed store query (rollup + ledger tail, both dialects) instead of up
to 400 usage_by_user calls.

Client/web: gateway 404/409 map to typed sentinels so the control plane
answers real status codes; expired sessions force re-login via a global
401 handler; keys tenant filter debounced; e2e now covers publish,
rollback, and failed login; CI builds the control-plane image.

Also carries the control-plane quality pass: shared admin helpers,
useAction/FormModal dedup, slices.SortFunc/cmp.Or modernization, ctx
threading, errgroup overview fan-out.
/code: fix declaration interleaving (parseTargets, scopeQuery, statusWriter,
userSeed, window, keyBelongsTo, interface checks), move user store impls
under internal/user to mirror the kv seam, propagate ctx in mock
PublishConfig, use t.Context() in tests, correct WithFunc names, trim
restating comments. /code-rs: fold repeated qualified paths into use
imports. /code-ts (new standard): type-only imports, expand UsersPage
one-liner JSX, dedup applySession, drive routes from the filtered nav
table, reuse jsonBody in ConfigPage. Simplify: scopedKey guard helper,
configBody carries expected_version, drop serveWeb's redundant
Content-Type.
Tenant-admin key mutations now authenticate with per-tenant gateway
tokens (CP_GATEWAY_TENANT_TOKENS, fail-closed) so the gateway's
AdminScope draws the boundary; the client-side guards and their
fleet-wide key scans are gone. The gateway redacts vendor_cost_micros
under a tenant scope and answers /admin/keys?ak= as an exact page.
Control-plane adds an X-Request-ID middleware threaded through logs and
proxied calls, key-list pagination passthrough, and an openapi drift
test. Docs catch up (root README, control-plane README, api.md,
deployment.md); release-control-plane.yml ships linux/darwin
amd64/arm64 binary tarballs beside the existing multi-arch images.
Browser -> control plane -> gateway binary on live Postgres/Redis:
scoped create/list, cross-tenant patch and create-takeover answered 404
by the gateway, fail-closed mutation without a tenant token, ak= filter
anti-probing, and vendor-cost redaction verified on billed traffic.
Mock create-takeover aligned to the gateway's 404 semantics.
Integration runs are uncached (-count=1); CI wires CP_TEST_GW_BIN.
/admin/usage/users and /admin/usage/series already zeroed
vendor_cost_micros for tenant tokens; /admin/usage leaked the margin
basis. Centralize the policy in AdminScope::sees_vendor_cost and apply
it in all three usage reads.
The MAX(id)-guarded insert was not a real CAS: under READ COMMITTED two
concurrent guarded publishes both see the old head and both insert, so
expected_version silently loses an update. All publishes now take
pg_advisory_xact_lock and guarded ones check the head under it; the
PG-gated test races eight contenders and admits exactly one.
The post-commit prune was a second round trip whose failure surfaced an
error for a publish that had already landed and notified the fleet; one
locked transaction makes retention all-or-nothing with the insert. Also
flatten the subscribe loop to while-let.
ALTER TABLE .. IF NOT EXISTS takes AccessExclusive even as a no-op, so
one transaction holding several tables' locks deadlocks against live
data transactions (40P01 in CI: finalize vs schema bootstrap). Per-DDL
transactions keep bootstrap serialization while never holding more than
one relation lock at a time.
On a shared database the config store outlives earlier suites'
publishes and wins over the file seed (the CI job runs the Rust suites
first), so the E2E gateway booted with a foreign config and never
became healthy. The test now provisions a uniquely named database and
force-drops it on cleanup, making it order-independent.
@CMGS
CMGS merged commit 617da54 into main Jul 18, 2026
7 checks passed
@CMGS
CMGS deleted the feat/control-plane-web branch July 18, 2026 16:08
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.

1 participant