feat(admin): CRUD handlers for guardrails / cache_policies / observability_exporters - #97
Conversation
…ility_exporters
Brings the standalone Admin API up to parity with the resource types
that aisix-core already understands and the gateway already honours
at runtime. Before this PR, those three resources existed as snapshot
tables and runtime hooks but had no admin endpoint — operators had
to hand-write etcd keys to configure them, which is exactly the
sharp edge an admin layer is supposed to file off.
Adds
- `guardrails_handlers.rs`, `cache_policies_handlers.rs`,
`observability_exporters_handlers.rs` — same template as
`provider_keys_handlers.rs`: validate JSON, reject duplicate name,
uuid v4 on POST, bump revision on PUT.
- 12 new `ConfigStore` trait methods (4 per resource), implemented
on both `InMemoryStore` and `EtcdConfigStore`.
- 6 new routes wired into `build_router`:
`/admin/v1/guardrails[/:id]`
`/admin/v1/cache_policies[/:id]`
`/admin/v1/observability_exporters[/:id]`
- 3 new etcd subkey constants (`guardrails`, `cache_policies`,
`observability_exporters`) — match the kind segments
`aisix-etcd::loader` already dispatches on, so writes from the
admin path land in the same prefix the watch supervisor reads.
- OpenAPI document expanded to include the new paths AND component
schemas (`Guardrail`, `CachePolicy`, `ObservabilityExporter`); the
forcing-function test from #96 walks the new entries.
- Five integration tests covering the happy path + duplicate-name
409 + bad-payload 400 + the loopback-only http endpoint guard on
observability exporters.
Re-exports `CachePolicy`, `ObservabilityExporter`, `ExporterKind`,
`validate_cache_policy`, `validate_observability_exporter` at the
`aisix_core` crate root so handlers can name them without reaching
into `aisix_core::models::*`.
Drive-by: README's "Admin handlers will follow" caveat is gone — the
list is now accurate.
Verified
- `cargo fmt --all --check` clean
- `cargo clippy --workspace --tests -- -D warnings` clean
- `cargo test -p aisix-admin` green (51 passed; +5 new)
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (9)
📝 WalkthroughWalkthroughThis PR extends the Admin API to support CRUD operations for three additional resource types: guardrails, cache policies, and observability exporters. The changes include handler implementations, store trait extensions, etcd/in-memory persistence, route wiring, OpenAPI documentation, and comprehensive test coverage. ChangesAdmin CRUD for Guardrails, Cache Policies, and Observability Exporters
Sequence DiagramsequenceDiagram
participant Client
participant Handler as Admin Handler
participant Store as ConfigStore Trait
participant Backend as Etcd / In-Memory
rect rgba(100, 150, 200, 0.5)
note over Client,Backend: CREATE Resource Flow
Client->>Handler: POST /admin/v1/guardrails<br/>(JSON payload)
Handler->>Handler: Validate JSON against schema
Handler->>Store: list_guardrails() (check uniqueness)
Store->>Backend: Retrieve existing entries
Backend-->>Store: Entries
Store-->>Handler: Existing guardrails
Handler->>Handler: Generate UUID, revision=1
Handler->>Store: put_guardrail(ResourceEntry)
Store->>Backend: Store entry with etcd key
Backend-->>Store: Success
Store-->>Handler: Persisted
Handler-->>Client: 201 Created + ResourceEntry
end
rect rgba(150, 150, 100, 0.5)
note over Client,Backend: UPDATE Resource Flow
Client->>Handler: PUT /admin/v1/guardrails/:id<br/>(JSON payload)
Handler->>Store: get_guardrail(id)
Store->>Backend: Fetch by id
Backend-->>Store: Existing entry (revision=N)
Store-->>Handler: ResourceEntry<Guardrail>
Handler->>Handler: Validate new payload
Handler->>Store: list_guardrails() (check uniqueness)
Handler->>Handler: Increment revision to N+1
Handler->>Store: put_guardrail(updated entry)
Store->>Backend: Update etcd entry
Backend-->>Store: Success
Store-->>Handler: Persisted
Handler-->>Client: 200 OK + Updated ResourceEntry
end
rect rgba(200, 100, 100, 0.5)
note over Client,Backend: DELETE Resource Flow
Client->>Handler: DELETE /admin/v1/guardrails/:id
Handler->>Store: delete_guardrail(id)
Store->>Backend: Remove from storage
Backend-->>Store: Deletion result (true/false)
Store-->>Handler: Success/NotFound
Handler-->>Client: 200 OK {deleted: true, id}
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~35 minutes Note 🎁 Summarized by CodeRabbit FreeYour organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above. Comment |
The supervisor's `apply_put`, `apply_delete`, and `clone_snapshot` helpers only handled `models` + `api_keys` — Phase B's ProviderKey and #97's Guardrail / CachePolicy / ObservabilityExporter were silently no-ops. Admin writes for those four resources landed in etcd fine, but the watch event got dropped and the proxy snapshot never updated, so dispatch saw a Model whose `provider_key_id` pointed at thin air. Smoke test #102 hit this: chat returned 500: bridge is misconfigured: model references unknown provider_key_id Fix is mechanical: extend the for-loops in apply_put + clone_snapshot and the match arms in apply_delete to cover every ResourceTable. Add `apply_put_propagates_every_resource_kind` + the matching delete test as forcing functions — any future resource type added to AisixSnapshot fails this test until the supervisor is updated. Verified - cargo fmt --all --check clean - cargo clippy --workspace --tests -- -D warnings clean - cargo test --workspace — 548 passed, 0 failed (was 546 + 2 new)
There was a problem hiding this comment.
Pull request overview
Adds standalone Admin API CRUD support for additional resource types that already exist in aisix-core and are honored by the gateway runtime, reducing the need for manual etcd edits.
Changes:
- Added CRUD handlers + routes for
guardrails,cache_policies, andobservability_exporters. - Extended
ConfigStore(and both in-memory + etcd implementations) with CRUD methods for the new resource types. - Expanded the admin OpenAPI document and updated README to reflect the new endpoints.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates documented Admin API capabilities/endpoints to include the new resources. |
| crates/aisix-core/src/lib.rs | Re-exports cache policy / observability exporter types and validators at the crate root. |
| crates/aisix-admin/src/store.rs | Extends ConfigStore and InMemoryStore to support CRUD for the three new resource types. |
| crates/aisix-admin/src/openapi.rs | Adds OpenAPI paths + component schemas for the new endpoints/resources (plus tests asserting presence). |
| crates/aisix-admin/src/observability_exporters_handlers.rs | New Axum CRUD handlers for /admin/v1/observability_exporters. |
| crates/aisix-admin/src/lib.rs | Wires new handler modules and mounts the new admin routes; adds integration tests for CRUD + validation. |
| crates/aisix-admin/src/guardrails_handlers.rs | New Axum CRUD handlers for /admin/v1/guardrails. |
| crates/aisix-admin/src/etcd_store.rs | Adds etcd subkeys + CRUD methods for the three new resources in EtcdConfigStore. |
| crates/aisix-admin/src/cache_policies_handlers.rs | New Axum CRUD handlers for /admin/v1/cache_policies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "type": "object", | ||
| "required": ["name", "kind"], | ||
| "properties": { | ||
| "name": {"type": "string", "example": "block-pii"}, | ||
| "enabled": {"type": "boolean", "default": true}, | ||
| "hook_point": {"type": "string", "enum": ["input", "output", "both"], "description": "Where in the request lifecycle the guardrail fires."}, | ||
| "fail_open": {"type": "boolean", "description": "Only honoured for kind=bedrock. true → request through on remote-API failure (with telemetry annotation); false → 422."}, | ||
| "kind": {"type": "string", "enum": ["keyword", "bedrock"]} | ||
| }, | ||
| "description": "Discriminated by `kind`. `keyword` carries a `patterns` array of literal/regex blocklist entries. `bedrock` carries `guardrail_id`, `guardrail_version`, `region`, `aws_credentials`, `latency_mode`. See `aisix-core::Guardrail` for the per-kind shape.", | ||
| "additionalProperties": true |
| "required": ["name", "kind"], | ||
| "properties": { | ||
| "name": {"type": "string", "minLength": 1, "maxLength": 120, "example": "honeycomb"}, | ||
| "enabled": {"type": "boolean", "default": true}, | ||
| "kind": {"type": "string", "enum": ["otlp_http"]}, | ||
| "endpoint": {"type": "string", "description": "Full URL of the OTLP/HTTP traces endpoint, including the `/v1/traces` path. Required when kind=otlp_http."}, | ||
| "headers": {"type": "object", "additionalProperties": {"type": "string"}, "description": "Static headers attached to every export. Plaintext at MVP — kine wire is mTLS-only."} | ||
| } |
…102) * feat(model): split provider_config inline into ProviderKey reference Realigns the standalone Model schema with the AISIX-Cloud control plane's normalised shape — the projection cp-api has been waiting for since PRD-09b §6 (the comment in mustMarshalModelKV calls this out as "Phase 2 swaps to {model, provider_key_id} with DP-side join, but requires Model.provider_config refactor across 26 DP files which is a separate PR" — that's this PR). Old shape (pre-#95 + this PR): { name, model: "<provider>/<id>", provider_config: { api_key, api_base } } New shape: { display_name, provider, model_name, provider_key_id } Where provider_key_id references a ProviderKey row (introduced as a top-level resource in #95) carrying secret + api_base. Routing models keep the same `routing` block but drop the upstream-config triple — the router resolves a target Model and dispatches against THAT model's provider_key_id. Why - One ProviderKey, many Models. Rotating the upstream secret used to require rewriting every Model row that embedded it; now it's a single PUT against the ProviderKey. - AISIX-Cloud parity. cp-api already has a `ProviderKey` table; managed-mode DPs need this shape to consume what cp-api projects into kine. - Snapshot-table integrity. The DP can validate at load time that every Model.provider_key_id resolves to a ProviderKey in the same snapshot, instead of carrying inline secrets it can't cross-check. Changes by area aisix-core - Model: replaced { name, model, provider_config } with { display_name, provider: Option<Provider>, model_name: Option<String>, provider_key_id: Option<String> }. Routing models set `routing` and leave the upstream triple as None. - Removed ProviderConfig struct entirely. - JSON Schema: oneOf encodes the direct-vs-routing XOR (direct ⇒ all three of provider/model_name/provider_key_id required; routing ⇒ all three forbidden). - Resource::name() now returns &display_name; ApiKey.allowed_models matches against the same field (already did, just renamed). aisix-gateway - BridgeContext gains `provider_key: Arc<ProviderKey>`. Constructor signature is now `new(request_id, model, provider_key)`. aisix-provider-{openai,anthropic,gemini,deepseek} - Bridge helpers (resolve_base / api_key / upstream_model) take `&BridgeContext` and read from ctx.provider_key + ctx.model rather than the now-gone provider_config. aisix-proxy - New `dispatch.rs` resolves both Model and ProviderKey from the snapshot before each per-endpoint handler builds BridgeContext. - Every endpoint (chat / completions / embeddings / messages / responses / rerank / images / audio / passthrough) updated to use the new resolver — no more inline `model.provider_config.api_key`. - 422 with a clear error envelope when a Model references a provider_key_id that isn't in the snapshot. Tests + fixtures - Every fixture across the workspace updated to the new JSON shape (~30 files: aisix-admin, aisix-cache, aisix-ratelimit, aisix-proxy, aisix-gateway, aisix-server, aisix-guardrails, aisix-etcd). Verified - `cargo fmt --all --check` clean - `cargo clippy --workspace --tests -- -D warnings` clean - `cargo test --workspace` green (520+ tests, 0 failures) Cross-repo follow-up - AISIX-Cloud's `mustMarshalModelKV` (internal/cpapi/resources/handlers.go) needs to switch from writing the inline `provider_config` shape to the new `{display_name, provider, model_name, provider_key_id}` shape. That's tracked separately and lands in AISIX-Cloud. * test: migrate Phase B fixtures — etcd_integration + e2e smoke The Phase B Model restructure commit landed the lib changes but the test fixtures in crates/aisix-admin/tests/etcd_integration.rs and tests/e2e/src/cases/smoke.test.ts still posted the old {name, model:"openai/...", provider_config:{...}} shape. Both surfaces fail in CI with the schema's "Additional properties are not allowed" rejection. - etcd_integration.rs: models_round_trip_through_real_etcd and loader_picks_up_every_admin_write switched to {display_name, provider, model_name, provider_key_id} - smoke.test.ts: now posts a ProviderKey first, then references its id from the Model — matches the production flow the dashboard drives. Adds AdminClient.createProviderKey for the test harness. * ci: kick the CI again — webhook missed 4d35529 * ci: trigger re-run for 4d35529 (webhook missed) * fix(messages): port PR #100 cross-provider /v1/messages to Phase B Model PR #100 (cross-provider /v1/messages — Anthropic protocol over non-Anthropic upstreams) landed on main with the pre-Phase-B Model API: model.provider() (method call), gemini_model(name, api_base) helpers, etc. After rebasing Phase B on top of #100, the Anthropic matrix tests + cross_provider_dispatch all stop compiling. This commit ports the survivors: - cross_provider_dispatch: switched to model.provider field access, picks up provider_key via dispatch::resolve_provider_key, threads it through BridgeContext::new(req_id, model, pk). - gemini_model / deepseek_model / anthropic_model_entry test helpers drop their api_base parameter — Phase B moves api_base onto ProviderKey, and the matrix harness now builds a fresh PK with the wiremock URI on every test. - Three test sites that still passed an extra api_base argument updated to the single-arg helper signature. * fix(supervisor): incremental watch must mirror every resource kind The supervisor's `apply_put`, `apply_delete`, and `clone_snapshot` helpers only handled `models` + `api_keys` — Phase B's ProviderKey and #97's Guardrail / CachePolicy / ObservabilityExporter were silently no-ops. Admin writes for those four resources landed in etcd fine, but the watch event got dropped and the proxy snapshot never updated, so dispatch saw a Model whose `provider_key_id` pointed at thin air. Smoke test #102 hit this: chat returned 500: bridge is misconfigured: model references unknown provider_key_id Fix is mechanical: extend the for-loops in apply_put + clone_snapshot and the match arms in apply_delete to cover every ResourceTable. Add `apply_put_propagates_every_resource_kind` + the matching delete test as forcing functions — any future resource type added to AisixSnapshot fails this test until the supervisor is updated. Verified - cargo fmt --all --check clean - cargo clippy --workspace --tests -- -D warnings clean - cargo test --workspace — 548 passed, 0 failed (was 546 + 2 new) * test(e2e): poll for snapshot readiness instead of fixed 500ms sleep The smoke test's `chat completion forwards to mock upstream` case intermittently fails on CI with `unknown provider_key_id` even though `a Model + ApiKey written via Admin API are visible to /v1/models` passes immediately before. The fixed-time `waitConfigPropagation()` times out in 500ms; on slower CI runners only the Model row makes it into the snapshot inside that window, while the ProviderKey row the Model references arrives a beat later — long enough for the chat call to look up `provider_key_id` and miss. waitConfigPropagation now accepts an optional `condition` callback that polls a positive readiness probe on a 50ms cadence with a 5s deadline. The smoke test uses two such probes: - After the Admin writes, poll /v1/models for the Model id (covers the Model row's propagation as before). - Before the chat assertion, poll the chat path itself, retrying as long as the response carries the `unknown provider_key_id` config error. That's the only signal that captures the *complete* snapshot state (Model + ProviderKey + ApiKey), since the proxy doesn't expose ProviderKey directly. The upstream-was-hit assertion still passes because both probe and the real call land on `/v1/chat/completions`. Local repro stays green; CI now has 5s of headroom for the second- event race instead of the old 0ms past the fixed sleep.
Summary
Brings the standalone Admin API up to parity with the resource types that
aisix-corealready models and the gateway already honours at runtime. Before this PR, those three resources existed as snapshot tables and runtime hooks but had no admin endpoint — operators had to hand-write etcd keys to configure them.What's new
guardrails_handlers.rs,cache_policies_handlers.rs,observability_exporters_handlers.rs— same template asprovider_keys_handlers.rs(validate JSON, reject duplicate name with 409, uuid v4 on POST, bump revision on PUT).ConfigStoretrait methods (4 per resource), implemented on bothInMemoryStoreandEtcdConfigStore.build_router:/admin/v1/guardrails[/:id]/admin/v1/cache_policies[/:id]/admin/v1/observability_exporters[/:id]guardrails,cache_policies,observability_exporters) — match the kind segmentsaisix-etcd::loaderalready dispatches on, so admin writes land in the same prefix the watch supervisor reads.Guardrail,CachePolicy,ObservabilityExporter). The forcing-function test from docs(admin): expand OpenAPI document to cover every mounted route #96 walks the new entries.Re-exports
CachePolicy,ObservabilityExporter,ExporterKind,validate_cache_policy,validate_observability_exporterat theaisix_corecrate root so handlers don't have to reach intoaisix_core::models::*.Drive-by
README's "Admin handlers will follow" caveat from #96 is removed — the list is now accurate.
Test plan
cargo fmt --all --checkcleancargo clippy --workspace --tests -- -D warningscleancargo test -p aisix-admin— 51 passed (+5 new integration tests)Summary by CodeRabbit
New Features
/admin/v1/endpoints with schema validation and duplicate name enforcement.Documentation