Ankusa.Routes.Router (ankusa v0.7.0)

Copy Markdown View Source

The route-management API: CRUD over route definitions, the global IP rules, and the dry run.

It listens on its own port (routes.admin.port), never on the ingest port: managing routes is an operator action, and mixing it into the capture surface would put a second, easily-forgotten endpoint next to the one that accepts provider traffic.

Authentication and scopes

Every route but GET /health needs a credential (routes.admin.auth, see Ankusa.Admin.Auth): 401 without one, 403 without the scope. Reading (GET, and the dry run POST /admin/routes/test) needs routes:read; every other write needs routes:write.

There are two surfaces over the same definitions. The operator surface, /admin/routes and /admin/ip-rules, spans tenants, so its scopes only count together with tenant:*: a principal limited to some tenants must not edit what every tenant's ingest is checked against. The tenant surface, /v1/tenants/:tenant/routes, needs tenant:<tenant> (or tenant:*) instead, and shows and changes only that tenant's routes: one that another tenant owns is 404, a body naming another tenant is 400, and a principal without the tenant is 403. A tenant that is not [A-Za-z0-9_-]{1,64} is 400 invalid_tenant.

Responding

MethodPathSuccess

| GET | /admin/routes?enabled=&limit=&cursor= | 200 {"routes": [...], "next_cursor": id | null} | | POST | /admin/routes | 201 route | | GET | /admin/routes/:id | 200 route | | PUT | /admin/routes/:id | 200 route (idempotent; creates if absent) | | PATCH | /admin/routes/:id | 200 route | | DELETE | /admin/routes/:id | 204 | | GET | /v1/tenants/:tenant/routes?enabled=&limit=&cursor= | 200 the page of that tenant's routes | | POST | /v1/tenants/:tenant/routes | 201 route owned by the tenant | | GET | /v1/tenants/:tenant/routes/:id | 200 route | | PUT | /v1/tenants/:tenant/routes/:id | 200 route (idempotent; creates if absent) | | PATCH | /v1/tenants/:tenant/routes/:id | 200 route | | DELETE | /v1/tenants/:tenant/routes/:id | 204 | | GET | /admin/ip-rules | 200 {"default": ..., "rules": [...]} | | PUT | /admin/ip-rules | 200 the stored rules | | POST | /admin/routes/test | 200 {"decision": ..., "reason": ..., "route_id": ..., "ip_rule": ...} | | GET | /health | 200 {"status": "ok", "routes": n} |

Errors are JSON objects with a stable error code: invalid_route (with field and message), duplicate_route (with conflicting_id, left out on the tenant surface when another tenant holds the id or the path), too_many_routes (with max_routes), invalid_query, invalid_tenant, invalid_ip_rules, invalid_request, invalid_body, not_found, and store_unavailable (a 503: the backing store could not be reached, or a write lost every race against concurrent writers — retry).

Writes are checked against the table they were validated against: two requests creating the same id, or two enabled routes for the same path and method, cannot both succeed however they interleave. PUT /admin/ip-rules replaces the whole rule set, so it requires both default and rules.

POST /admin/routes/test is the dry run: it answers "what would happen to this request" without capturing anything and without touching the decision cache, so an operator can validate a route change before it goes live.

Contract

priv/openapi/admin.v1.yaml is the HTTP contract for these endpoints (the routes tag), and it is the source of truth: test/ankusa/routes/router_openapi_test.exs builds each documented request from the examples in it and asserts the response against the documented schema and example, so this module answers what the document says or the suite fails. Change one, change the other.

Summary

Functions

Callback implementation for Plug.call/2.

Callback implementation for Plug.init/1.

Functions

call(conn, opts)

Callback implementation for Plug.call/2.

init(opts)

Callback implementation for Plug.init/1.