-
Notifications
You must be signed in to change notification settings - Fork 0
feat(control-plane): version all API routes under /api/v2 #534
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
a790466
6778a74
f330c5e
0c5004c
87cc8ba
801b0e8
2870332
9ebe570
bdaddbe
c132e88
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 |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # ADR-0109: Version all control-plane routes under `/api/v2` | ||
|
|
||
| - **Status:** Accepted | ||
| - **Date:** 2026-07-28 | ||
| - **Deciders:** @stephane-segning, @leghadjeu-christian | ||
|
|
||
| ## Context | ||
|
|
||
| Every route in the control plane was registered at the root path with no version prefix. Three | ||
| different auth models (OIDC-gated dashboard routes, shared-bearer runner-internal routes, and | ||
| admin routes) shared one flat, unversioned namespace. There was no mechanism to introduce a | ||
| breaking API change without affecting all consumers simultaneously. | ||
|
|
||
| ## Decision | ||
|
|
||
| All consumer-facing routes are nested under `/api/v2` using Axum's `.nest()`. The router is | ||
| split into two functions: | ||
|
|
||
| - `api_v2_router()` — returns all versioned routes as a `Router<AppState>` | ||
| - `app()` — mounts the versioned sub-router plus the infra probes | ||
|
|
||
| Health probes (`/healthz`, `/readyz`) and `/metrics` stay at root because they are consumed by | ||
| Kubernetes and Prometheus respectively, not by API clients. | ||
|
|
||
| The `/api/v2` prefix is carried in the env var, not appended by client constructors. Both internal | ||
| and external clients trim trailing slashes and use the value as-is. The Helm chart values are | ||
| updated to include the prefix: | ||
|
|
||
| - `apps/web`: `controlPlaneUrl()` uses `AUTH_BACKEND_URL` as-is — chart sets `…:8080/api/v2` | ||
| - `lci` CLI: `ApiClient::new()` trims trailing slashes — `api_url` must include `/api/v2` | ||
| - `agent-clients`: `ControlPlaneClient::new()` trims trailing slashes — `CONTROL_PLANE_INTERNAL_URL` must include `/api/v2` | ||
|
|
||
| This is consistent: every consumer has one place (the env var or config value) where the full API | ||
| base is set. The Helm chart update is in the companion PR (ADORSYS-GIS/ai-helm). | ||
|
|
||
| The cutover is hard — old flat paths return 404 immediately after deployment. The chart update | ||
| must be deployed in the same window as the new image. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - The single `api_v2_router()` function is the canonical list of all versioned routes; adding a | ||
| route requires touching one place. | ||
| - Operators must ensure `CONTROL_PLANE_INTERNAL_URL` and `AUTH_BACKEND_URL` include `/api/v2`. | ||
| The chart default values are updated in the companion Helm PR (ADORSYS-GIS/ai-helm#817). | ||
| - Local dev env vars must also be updated if set explicitly (e.g. `CONTROL_PLANE_URL=http://localhost:8080/api/v2`). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -318,12 +318,9 @@ fn db_readiness(has_pool: bool, ping_ok: bool, allow_no_db: bool) -> DbReadiness | |
| } | ||
| } | ||
|
|
||
| fn app(state: AppState) -> Router { | ||
| fn api_v2_router() -> Router<AppState> { | ||
| Router::new() | ||
| .route("/healthz", get(liveness)) | ||
| .route("/readyz", get(readiness)) | ||
| .route("/metrics", get(metrics_endpoint)) | ||
| // Path-scoped webhook ingress — one route per forge, no header-sniffing. | ||
| // Webhook ingress: path-scoped per forge, no header-sniffing. | ||
| .route( | ||
| "/webhook/github", | ||
| post(webhook::github_webhook).layer(DefaultBodyLimit::max(webhook::MAX_BODY_BYTES)), | ||
|
|
@@ -448,6 +445,14 @@ fn app(state: AppState) -> Router { | |
| "/internal/tasks/{id}/propose-pr", | ||
| post(internal::propose_pr).layer(DefaultBodyLimit::max(32 * 1024 * 1024)), | ||
| ) | ||
| } | ||
|
|
||
| fn app(state: AppState) -> Router { | ||
| Router::new() | ||
| .route("/healthz", get(liveness)) | ||
| .route("/readyz", get(readiness)) | ||
| .route("/metrics", get(metrics_endpoint)) | ||
|
leghadjeu-christian marked this conversation as resolved.
Contributor
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. lci CLI compiled-in default URL stays bare → /me 404s against /api/v2 server The server now nests every consumer-facing route under The maintainer's reply addresses the Helm-chart-managed env vars ( Evidence: services/control-plane/src/main.rs:454
|
||
| .nest("/api/v2", api_v2_router()) | ||
| .layer(axum::middleware::from_fn(track_http_metrics)) | ||
| .with_state(state) | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.