Skip to content

feat(admin): CRUD handlers for guardrails / cache_policies / observability_exporters - #97

Merged
moonming merged 1 commit into
mainfrom
feat/admin-handlers-extras
May 7, 2026
Merged

feat(admin): CRUD handlers for guardrails / cache_policies / observability_exporters#97
moonming merged 1 commit into
mainfrom
feat/admin-handlers-extras

Conversation

@moonming

@moonming moonming commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Brings the standalone Admin API up to parity with the resource types that aisix-core already 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

  • 3 handler files: guardrails_handlers.rs, cache_policies_handlers.rs, observability_exporters_handlers.rs — same template as provider_keys_handlers.rs (validate JSON, reject duplicate name with 409, 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 etcd subkey constants (guardrails, cache_policies, observability_exporters) — match the kind segments aisix-etcd::loader already dispatches on, so admin writes land in the same prefix the watch supervisor reads.
  • OpenAPI doc expanded to include the new paths + component schemas (Guardrail, CachePolicy, ObservabilityExporter). The forcing-function test from docs(admin): expand OpenAPI document to cover every mounted route #96 walks the new entries.
  • 5 integration tests covering 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 don't have to reach into aisix_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 --check clean
  • cargo clippy --workspace --tests -- -D warnings clean
  • cargo test -p aisix-admin — 51 passed (+5 new integration tests)
  • OpenAPI forcing-function test asserts every new path + schema exists

Summary by CodeRabbit

  • New Features

    • Admin API now supports full CRUD operations for guardrails, cache policies, and observability exporters via /admin/v1/ endpoints with schema validation and duplicate name enforcement.
  • Documentation

    • Updated README and OpenAPI spec to reflect newly available admin routes and entities.

…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)
@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: af70e995-6f05-4c89-8bec-d60257322339

📥 Commits

Reviewing files that changed from the base of the PR and between cca5b6f and 3c86314.

📒 Files selected for processing (9)
  • README.md
  • crates/aisix-admin/src/cache_policies_handlers.rs
  • crates/aisix-admin/src/etcd_store.rs
  • crates/aisix-admin/src/guardrails_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/observability_exporters_handlers.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/src/store.rs
  • crates/aisix-core/src/lib.rs

📝 Walkthrough

Walkthrough

This 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.

Changes

Admin CRUD for Guardrails, Cache Policies, and Observability Exporters

Layer / File(s) Summary
Store Trait & Implementations
crates/aisix-admin/src/store.rs
ConfigStore trait extended with guardrail, cache policy, and observability exporter CRUD methods; InMemoryStore implements put_, get_, list_, delete_ for each type with DashMap backing.
Etcd Persistence
crates/aisix-admin/src/etcd_store.rs
EtcdConfigStore implements the trait methods for three new resource types using subkey constants (guardrails, cache_policies, observability_exporters); etcd value layout and revision tracking remain consistent with existing patterns.
HTTP Handler Implementations
crates/aisix-admin/src/guardrails_handlers.rs,
crates/aisix-admin/src/cache_policies_handlers.rs,
crates/aisix-admin/src/observability_exporters_handlers.rs
Each handler module implements list/get/create/update/delete endpoints; shared semantics include JSON-schema validation, duplicate-name conflict detection (409), UUIDv4 ID generation on POST, revision bumping on PUT, and 404 on missing resources.
Router Wiring & Public API
crates/aisix-admin/src/lib.rs,
crates/aisix-core/src/lib.rs
Router builder registers authenticated endpoints under /admin/v1/guardrails, /admin/v1/cache_policies, and /admin/v1/observability_exporters; aisix-core public re-exports updated to include validate_cache_policy and validate_observability_exporter.
OpenAPI Documentation
crates/aisix-admin/src/openapi.rs
Hand-written OpenAPI spec extended with new paths (list/create and {id} update/delete variants), request/response schemas, and component definitions for Guardrail, CachePolicy, and ObservabilityExporter; documentation tests verify all mounted routes and required schemas are present.
Tests & Documentation
crates/aisix-admin/src/lib.rs,
README.md
New Tokio async tests for CRUD flows on all three resource types, including payload helpers, duplicate-name rejection, revision tracking, and schema validation; README clarified to explicitly list the new Admin API resource types.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes


Note

🎁 Summarized by CodeRabbit Free

Your 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 @coderabbitai help to get the list of available commands and usage tips.

@moonming
moonming merged commit 6a0c013 into main May 7, 2026
7 checks passed
@moonming
moonming deleted the feat/admin-handlers-extras branch May 7, 2026 04:25
moonming added a commit that referenced this pull request May 7, 2026
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)
Copilot AI review requested due to automatic review settings May 7, 2026 06:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and observability_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.

Comment on lines +318 to +328
"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
Comment on lines +345 to +352
"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."}
}
moonming added a commit that referenced this pull request May 7, 2026
…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.
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.

2 participants