[Proposal] Separate API-Facing Resource Types from Internal Runtime Model in Gateway Controller #1259
Replies: 1 comment
-
Having a small question: For kinds like MCP, GraphQL etc where policy targets are derived from the payload rather than the URL path, how do we consider a "route" from Envoy vs Policy Engine perspective?. MCP for example: Envoy would see a single route (e.g., POST /mcp), but the Policy Engine needs per-tool (or per-operation) granularity for policy attachment. The current RouteConfig struct has Path and Method, which looks similar to a HTTP-level routing. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
Refactor the gateway controller's internal model to cleanly separate API-facing resource types (used for REST API communication and persistence) from internal runtime types (used for Envoy and Policy Engine configuration via xDS). The core changes are:
RuntimeConfigstruct as the canonical internal representation for all deployable resources, replacing the current pattern of forcing every resource intoAPIConfigurationDeployableResourceGo interface withKind()andHandle()methods that all resource configuration types implementTransformerinterface to connectDeployableResource(input) toRuntimeConfig(output), with a Kind-keyed registry and per-resource-type implementations in a dedicatedpkg/transformer/packageAPIDeploymentService,LLMDeploymentService,MCPDeploymentService) into a single pipeline that dispatches via the Kind registryStoredConfigtoDeployment; merge thedeployments+deployment_configstwo-table schema into a single table with a discriminatedresource_configJSON columnUNIQUE(display_name, version, gateway_id)identity withUNIQUE(gateway_id, kind, handle)— handle-based identity eliminates the{displayName}:{version}composite key patternmap[string]*StoredConfig(O(n) kind lookups) with a two-levelKind → Handle → RuntimeConfigmapconfig_dumpadmin endpoint to return RuntimeConfig state organized by Kind and HandleThe external REST API contract remains fully backward compatible. Breaking changes are limited to the database schema and internal implementation.
Motivation
The Problem: Everything Is Forced Into APIConfiguration
Today, the gateway controller handles five resource types -- REST APIs, LLM providers, LLM proxies, MCP proxies, and WebSub APIs -- but all of them are forced through a single
APIConfigurationstruct before storage and xDS translation.Here is the current
StoredConfigstruct (frompkg/models/stored_config.go):And the current
Transformerinterface (frompkg/utils/transformer.go):This creates several concrete problems:
Semantic loss. An LLM provider is not a REST API. Forcing it into
APIConfigurationstrips away its meaning -- access control modes (allow_all/deny_all), provider templates, and model-level configuration become invisible to the internal model. The transformer must reconstruct API-shaped constructs (fake operations, synthetic routes) to carry non-API concepts through a struct that was never designed for them.Unsafe type assertions everywhere. The
SourceConfiguration anyfield requiresinterface{}type assertions scattered across REST handlers and storage code. A typo or wrong assertion produces runtime panics, not compile-time errors. Every handler that returns a non-REST-API resource must castSourceConfigurationto the correct type manually.No compile-time type safety for dispatch. The transformer accepts
anyas input. There is no Go compiler enforcement that a resource type provides the right data or that the correct transformer is used. Dispatch relies on string-based Kind matching with manualswitchstatements.Fragile identity model. Resources are identified by a
{displayName}:{version}composite key (GetCompositeKey()) that requires extracting display name and version from deeply nested spec data, with special-case handling for WebSub APIs. This composite key leaks API-level concepts into every layer.Split-table storage confusion. The database splits a single logical entity across two tables:
deployments(metadata: display_name, version, kind, handle, status) anddeployment_configs(payload:configurationasAPIConfiguration,source_configurationasany), joined by a shared primary key. This 1:1 split adds unnecessary complexity and the dual payload columns create ambiguity about which is the source of truth.Duplicated deployment pipelines. Three separate deployment services (
APIDeploymentService,LLMDeploymentService,MCPDeploymentService) each duplicate the parse → validate → transform → save flow. REST APIs skip transformation entirely sinceAPIConfigurationis already the target type. Adding a new resource kind requires creating yet another deployment service with the same boilerplate.Non-transactional persistence. The current
saveOrUpdateConfig()writes to the DB then to the in-memory store sequentially. On DB failure, it attempts a best-effort memory rollback — not a true atomic operation. Multiple controller instances sharing the same database can observe inconsistent state.Why This Matters Now
The platform is expanding beyond REST APIs. LLM providers, LLM proxies, and MCP proxies are first-class resource types with their own semantics, but the internal model treats them as second-class citizens that must disguise themselves as REST APIs. Every new resource type added under the current design increases the surface area of unsafe type assertions, deepens the semantic mismatch, and makes the codebase harder to reason about. This is the right time to establish a clean internal model before the resource type count grows further.
Proposal
1. RuntimeConfig: The Canonical Internal Representation
A new
RuntimeConfigstruct replacesAPIConfigurationas the internal model consumed by the xDS translator. It is resource-agnostic -- it describes what Envoy and the Policy Engine need, not what the original resource type was.Key design properties:
Ephemeral. RuntimeConfig is held only in the in-memory ConfigStore, never persisted to the database. It is derived from stored resources at deployment time and rebuilt from stored resources on startup. This means the original API resource is always the single source of truth, RuntimeConfig struct changes between controller versions do not require database migrations, and there is no risk of derived data drifting out of sync.
Embedded sub-structs. Rather than a flat struct with dozens of fields, RuntimeConfig uses logical groupings (
UpstreamConfig,TLSConfig,ListenerConfig) so functions can accept only the subset they need.Per-route policy chains only. There is no distinction between "API-level" and "operation-level" policies at the RuntimeConfig level. The transformer merges all policy sources into per-route chains before placing them into RuntimeConfig. At the Router and Policy Engine, everything is route-level.
No resource-specific fields copied as-is. LLM access control modes, provider template references, and model-level configuration are transformed into per-route policies and route metadata by the transformer. RuntimeConfig holds derived configuration, not a mirror of the source.
In-memory ConfigStore uses a two-level map (
Kind -> Handle -> RuntimeConfig) for O(1) lookup by(Kind, Handle)and O(1) enumeration of all resources of a given Kind. This replaces the current flatmap[string]*StoredConfigwhich requires O(n) scans forGetAllByKind()andGetByKindAndHandle().config_dump endpoint. The existing admin API
config_dumpendpoint (port 9092) is updated to return RuntimeConfig state from the ConfigStore, organized by Kind and Handle, instead of the currentAPIConfiguration-based format.2. DeployableResource: A Common Interface for All Resource Types
A
DeployableResourceGo interface provides compile-time type safety for all resource types entering the deployment pipeline:All five resource configuration types implement this interface:
APIConfiguration(RestApi),LLMProviderConfiguration(LlmProvider),LLMProxyConfiguration(LlmProxy),MCPProxyConfiguration(Mcp), and a newWebhookAPIConfiguration(WebSubApi). Note:WebhookAPIConfigurationdoes not exist today as a standalone type — WebSubApi currently usesAPIConfigurationwith a union pattern (Spec.AsWebhookAPIData()). A thin wrapper type will be introduced.The interface is intentionally minimal -- only what the deployment pipeline needs for identification.
Name(),Version(), andTranslator()are deliberately excluded: display names and version information remain accessible from each concrete type's spec data but are not part of the common interface contract.Translator()was considered but rejected (see Alternatives) -- transformer dispatch uses a Kind-keyed registry instead. Handle (frommetadata.name) is the sole resource identity. The{displayName}:{version}composite key pattern is eliminated entirely.Since these types are OpenAPI-generated (in
api/generated/generated.go), the interface method implementations are placed in separate Go source files (e.g.,api_configuration_deployable.go,llm_provider_configuration_deployable.go) so they survive code regeneration.3. Redesigned Transformer: DeployableResource to RuntimeConfig
The Transformer interface is redesigned with typed input and output:
Each resource type gets a dedicated Transformer implementation:
RestApiTransformerAPIConfigurationis already the target type)LlmProviderTransformerLlmProxyTransformerMcpTransformerWebSubApiTransformerDispatch uses a Kind-keyed transformer registry (
map[string]Transformer) initialized at startup. The deployment pipeline looks up the transformer viatransformers[resource.Kind()]. This is a simple, explicit dispatch mechanism that avoids the problems of embedding infrastructure references in data structs (see Alternatives).Each Transformer is a singleton per Kind, constructed once with a reference to the storage layer (constructor injection). Within the
Transformmethod, transformers can query the database to resolve cross-resource references (e.g., LLM proxy resolving its referenced provider).All transformer code moves from
pkg/utils/(a catch-all package) to a dedicatedpkg/transformer/package.4. Deployment: Renamed Storage with Discriminated JSON
StoredConfigis renamed toDeployment(aligning with the existingdeploymentstable name). The two-table pattern is merged into a single table with a discriminated JSON column:The
deployment_configstable is eliminated — no reason for a 1:1 split. TheKindfield determines deserialization type at load time — noanyorinterface{}required. TheUNIQUE(display_name, version, gateway_id)constraint is replaced byUNIQUE(gateway_id, kind, handle). Handles can be duplicated across different Kinds (an LLM provider and an LLM proxy may both be named "my-service"), sokindis part of the uniqueness constraint.Deployment persistence and transformation are wrapped in a SQLite transaction. If the Transformer returns an error, the transaction rolls back -- no partially deployed resources remain in the database. This is critical because multiple controller instances may share the same database.
5. Ordered Startup with Dependency Resolution
On startup, the controller loads resources from the database and transforms them into RuntimeConfig instances. Resources are loaded in dependency order:
If a dependent resource references a missing dependency, it is marked as failed with a clear error message. Other resources continue loading normally.
6. REST API Backward Compatibility
The external REST API contract is preserved exactly as-is. Each resource type's API-facing struct (
APIConfiguration,LLMProviderConfiguration, etc.) continues to serve REST request/response payloads. REST GET handlers read the original resource configuration from the database (deserialized using the Kind discriminator), not from the in-memory ConfigStore. The ConfigStore holds only RuntimeConfig.Architecture Diagram
flowchart TD A["REST API Layer POST /apis, /llm-providers, ..."] -->|parse & validate| B["DeployableResource .Kind() .Handle()"] subgraph TX ["SQLite Transaction"] direction TB P["Persist Deployment"] --> T["Transform"] T -->|rollback on error| P end B --> TX P --> C[("SQLite DB single deployments table UNIQUE(gateway_id, kind, handle) resource_config JSON")] B -->|lookup by Kind| D["Transformer Registry map[Kind]Transformer"] D --> T C -->|cross-resource lookups| D T -->|RuntimeConfig| F["In-Memory ConfigStore Kind → Handle → RuntimeConfig"] F --> G["xDS Translator reads only RuntimeConfig"] G --> H["Envoy + Policy Engine"] F -.->|GET /config_dump| I["Admin API port 9092"] style A fill:#4a90d9,color:#fff style C fill:#f5a623,color:#fff style TX fill:#f0f0f0,stroke:#999,stroke-dasharray: 5 5 style F fill:#7ed321,color:#fff style H fill:#d0021b,color:#fff style I fill:#9b59b6,color:#fffAlternatives Considered
Alternative 1: Store RuntimeConfig in the Database Alongside Original Resources
Persist both the original resource configuration and the derived RuntimeConfig in the database, so startup does not require re-transformation.
Rejected because: This creates a dual-source-of-truth problem. If transformation logic changes between controller versions, the stored RuntimeConfig becomes stale and must be migrated or invalidated. The startup cost of re-transformation is negligible for typical deployments (tens to low hundreds of resources), and the single-source-of-truth benefit of ephemeral RuntimeConfig significantly outweighs the sub-second startup cost.
Alternative 2:
Translator()Method on DeployableResourceAdd a
Translator() Transformermethod to theDeployableResourceinterface so each resource is self-describing and the pipeline callsresource.Translator().Transform(resource)with no registry needed.Rejected because: Transformers are stateful singletons constructed with a DB reference.
DeployableResourcetypes are data structs deserialized from JSON -- they don't naturally hold infrastructure references. This would require injecting a Transformer into every deserialized config struct before use. It also creates a circular dependency risk (DeployableResourcereferencesTransformerwhich acceptsDeployableResource), and complicates the separate-file strategy for OpenAPI-generated types. A Kind-keyed map is simpler, has no injection problem, no import cycle risk, and is trivially testable (swap the map entry in tests).Alternative 3: Use Generics Instead of a Common Interface
Define
Transformer[T DeployableResource]with generic type parameters so each transformer receives its concrete type directly.Not rejected outright, but the current design favors the simpler interface-based approach. The deployment pipeline needs to work with heterogeneous resource types through a single code path. Generic transformers would require type-switch dispatch at some point, losing the benefit of the Kind-keyed registry pattern. If the Go generics story improves in a way that makes this cleaner, it can be revisited.
Impact Analysis
Benefits
anyorinterface{}type assertions in REST handlers or storage code. The Go compiler enforces that resource types implement the required interface.DeployableResource, creating a Transformer inpkg/transformer/, and adding a Kind constant. No changes to RuntimeConfig, xDS translator, or storage core logic.metadata.name) replaces the fragile{displayName}:{version}composite key everywhere.APIDeploymentService,LLMDeploymentService,MCPDeploymentService) that duplicate the same flow.deploymentstable with oneresource_configJSON column replaces the current two-table split (deployments+deployment_configs) with dual payload columns.Risks and Mitigations
References
StoredConfigstruct:gateway/gateway-controller/pkg/models/stored_config.goTransformerinterface:gateway/gateway-controller/pkg/utils/transformer.goLLMTransformer:gateway/gateway-controller/pkg/utils/llm_transformer.goMCPTransformer:gateway/gateway-controller/pkg/utils/mcp_transformer.gogateway/gateway-controller/pkg/utils/api_deployment.go,llm_deployment.go,mcp_deployment.gogateway/gateway-controller/pkg/xds/translator.gogateway/gateway-controller/resources/gateway-controller-db.sqlgateway/gateway-controller/pkg/storage/sql_store.gogateway/gateway-controller/pkg/storage/memory.gogateway/gateway-controller/pkg/adminserver/server.goDocument Version: 1.0
Last Updated: 2026-02-24
Status: Proposed
Beta Was this translation helpful? Give feedback.
All reactions