The operator HTTP API: health, Prometheus metrics, the redacted config, the AsyncAPI document of the channels this instance publishes to, the dead-letter queue, replay jobs, and this node's quarantine pen (list and purge).
It exists so operating Ankusa does not require an Elixir shell. Everything
here had an IEx-only affordance before (Ankusa.Dispatch.replay/2,
Ankusa.Edge.Quarantine.recent/2, :sys.get_state/1-style poking); this is
the same surface over HTTP.
Authentication and scopes
Every route but GET /health and GET /ready needs a credential
(admin.auth, see Ankusa.Admin.Auth): 401 without one, 403 when the
caller's scopes (Ankusa.Principal) do not cover the route. Scopes are
checked before anything else, so a caller without them learns nothing about
this node's roles or data.
| Route | Scope |
|---|---|
GET /metrics, GET /v1/wal | metrics:read |
GET /v1/config, GET /asyncapi.json | config:read |
PUT /v1/log-level | config:write + tenant:* |
POST /v1/config/reload | config:write + tenant:* |
GET /v1/dlq, DELETE /v1/dlq | dlq:read / dlq:write + a tenant (filtered) |
POST /v1/replays | replays:write + a tenant (filtered), and dlq:write / quarantine:write for those kinds |
GET /v1/replays[/:id] | replays:read + a tenant (filtered) |
PATCH /v1/replays/:id | replays:write + a tenant (filtered) |
GET /v1/quarantine, DELETE /v1/quarantine | quarantine:read / quarantine:write + a tenant (filtered) |
GET /v1/tenants/:tenant/sources[/:name] | sources:read + tenant:<tenant> |
POST/PUT/DELETE /v1/tenants/:tenant/sources[/:name] | sources:write + tenant:<tenant> |
GET /v1/rate-limits | rate_limits:read + a tenant (filtered) |
GET/PUT/DELETE /v1/tenants/:tenant/rate-limit | rate_limits:read / rate_limits:write + tenant:<tenant> |
DELETE /v1/tenants/:tenant?confirm=<tenant>, GET /v1/tenants/:tenant/erase | tenants:write + tenant:* |
The DLQ, the quarantine pen, replay jobs and the rate-limit listing hold every
tenant's rows, so they are filtered by tenant (Auth.authorize_tenants/4).
A caller limited to some tenant:<id> scopes gets only those tenants' rows,
whatever it asks: the filter is forced server-side. ?tenant=<id> (the body
key tenant of POST /v1/replays) narrows the answer to one tenant, and
naming a tenant the caller does not hold is 403. Only tenant:* reads or
changes them unfiltered. A quarantine purge deletes, and a replay touches,
only the caller's tenants' rows; a replay job is visible to (and steerable
by) a limited caller only when it is scoped to tenants that caller holds.
/v1/config is still redacted, because many people may hold config:read.
Node-local by design
A route that needs a role this node does not run returns
409 role_not_enabled rather than an empty success: the DLQ is the dispatch
node's store, the quarantine list is the edge node's, so a wrong-node
answer must be distinguishable from "nothing there". GET /v1/wal is the
same kind of answer: it describes this node's own store, and under
wal.type: none there is no queue to describe (409 wal_disabled).
Aggregating across nodes is the operator's job (scrape every admin port).
Summary
Functions
Callback implementation for Plug.call/2.
Callback implementation for Plug.init/1.