[Proposal][Design] Per-Operation Upstream Override on OperationRequest #1717
mehara-rothila
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Background
The current
OperationRequestschema does not support per-operation upstream routing. All operations of a REST API share the API-levelupstream.main(and optionallyupstream.sandbox), as defined inopenapi.yaml.In real-world deployments, a single API often fronts a microservice cluster where each resource maps to a different backend service. Today this requires creating a separate REST API per backend, which fragments the API contract and breaks the "one logical API, many implementations" model.
This discussion proposes adding an optional
upstreamfield onOperationRequestso a single REST API can route its operations to different backends.It builds on prior decisions in:
upstreamDefinitionsschema: defined the named upstream definition system withbasePathand reusable upstream clusters underupstreamDefinitions.clear_route_cacheincompatibility by splitting responsibilities across an ext_proc filter (setsx-target-upstreamand clears route cache) and a Lua filter (modifies:path/:methodwithout clearing route cache). The route usescluster_headerpointing tox-target-upstreamwithrequest_headers_to_removeto strip the internal header before forwarding upstream.Proposed schema
A new optional
upstreamfield onOperationRequest. The shape is anOperationUpstreamwrapper that mirrors the API-levelUpstreamshape with separatemainandsandboxsub-fields. Each sub-field is anOperationUpstreamTarget— a strict subset ofUpstreamDefinitioncontaining onlyurlandref(mutually exclusive viaoneOf). Theauthsub-field is omitted entirely so per-op upstream credentials cannot be expressed in the schema (see Validation rules).Schema details
OperationUpstream (wrapper)
OperationUpstreamTarget (leaf)
Design notes:
mainandsandboxare optional. Either sub-field can be omitted independently.authsub-field is intentionally omitted fromOperationUpstreamTarget. Operation-level credentials belong in header-injection or OAuth policies attached to the operation. Becauseauthis not in the schema at all, consumers cannot set it and the handler does not need a runtime carve-out.reffield is reserved for forward compatibility with a futureupstreamDefinitionsregistry at the Platform API layer. No such registry exists today, so any non-emptyrefis rejected with 400 by the handler before DB write (see Validation rules). When the registry lands, this rejection lifts and refs resolve against the API'supstreamDefinitionslist.Resolution order
Per-operation upstream is environment-scoped. The gateway resolves independently for each vhost:
operation.request.upstream.mainupstream.mainoperation.request.upstream.sandboxupstream.sandboxEither sub-field can be omitted. An omitted sub-field falls back to the API-level upstream for that environment. This gives four valid configurations per operation:
upstream.mainupstream.sandboxValidation rules
urlandrefare mutually exclusive within a single target, enforced at the schema layer viaoneOf.reffield is rejected unconditionally by the Platform API handler: any non-emptyrefundermainorsandboxreturns400 Bad Requestwith sentinelErrUpstreamRefNotSupported. The Platform API does not expose anupstreamDefinitionsregistry (the REST API schema has noupstreamDefinitionsfield), so refs have no target to resolve against. Schema keepsreffor forward compatibility; it will be accepted only when definitions are exposed at this layer.auth(see Schema details above).End-to-end feature scope
platform-api(control plane)OperationUpstreamwrapper schema andOperationUpstreamTargetleaf schema added toopenapi.yaml.OperationRequestschema gains the optionalupstreamfield as a$reftoOperationUpstream.api.OperationRequest.Upstreamis*api.OperationUpstream) carries the field.model.OperationRequest) reuses the existingUpstreamConfigtype (which already hasMain/Sandboxshape) for the per-op field.Authis representable internally viaUpstreamEndpoint.Authbut is never populated from wire input and is never emitted back to wire.internal/utils/api.goround-trip the wrapper with nil-guarding:operationUpstreamToModel(*api.OperationUpstream) *model.UpstreamConfigupstreamConfigToOperationUpstream(*model.UpstreamConfig) *api.OperationUpstreamBuildAPIDeploymentYAML) emits per-opupstreamwhen present and omits it otherwise.upstreamon create and update so the field survivesapi.yamlround-trips.MergeRESTAPIDetails) stamps per-op upstream from the user body onto extracted operations by(Method, Path)key.refvalue (undermainorsandbox) before DB write. A sentinel errorErrUpstreamRefNotSupportedis returned as 400 Bad Request — the Platform API does not yet expose anupstreamDefinitionsregistry, so refs have no target to resolve against at this layer.rest_apis.configurationJSON blob. No database migration required.gateway-controllerOperationconfig type gains theUpstreamfield as anOperationUpstreamwrapper.pkg/transform/restapi.goTransform()):op.Upstream.Maindrives the main vhost route's cluster key.op.Upstream.Sandboxdrives the sandbox vhost route's cluster key.UseClusterHeaderandDefaultClusterare disabled for that route.op.Upstream.Sandbox != nil).pkg/xds/translator.gotranslateAPIConfig()):mainClusterNameand sandbox routes withsbClusterNamefor every operation. This proposal extends it with the same per-op cluster derivation logic as the primary path: readop.Upstream.Main/op.Upstream.Sandbox, register per-op clusters via the shared helper, and pin each vhost route to the resolved cluster. The sandbox loop gains the same guard: skip override whenop.Upstream.Sandboxis set.urloverrides).urloverrides, a synthetic Envoy cluster is registered.op_<first-6-bytes-of-SHA256-in-hex>), so two operations pointing at the same backend collapse to a single cluster (xDS-snapshot friendly) and distinct URLs get distinct clusters.addPerOpUpstreamClusteris idempotent — identical URLs reuse the same cluster entry.refresolution. Per-oprefvalues are resolved to a URL byresolveUpstreamURL, which looks up the referencedupstreamDefinitionby name and returns its first upstream URL. That URL is then hashed with SHA-256, and the cluster key becomesop_<first-12-hex-chars>(e.g.,op_a1b2c3d4e5f6). Two operations referencing the same definition collapse to the same cluster (dedup); distinct URLs never collide. Per-op upstreams never reuse the API-level scoped naming convention (upstream_<kind>_<apiId>_<name>), andEnvoyClusterNameis intentionally left empty because per-op routes do not use thecluster_headerdynamic-selection path.urlor resolvedref) always have the same simple structure:BasePathfrom the URL path, a single endpoint from the URL host and port, and TLS enabled if the scheme ishttps. No other settings (auth, timeouts, load-balancing) are inherited from a referencedupstreamDefinition— only the resolved URL is used.cluster_headerdynamic-selection flow from [Proposal] Rewrite Path and Dynamic Endpoint - Resolve Conflicting Policies #1298. When a per-op override is present,restapi.gosetsUseClusterHeader = falseandDefaultCluster = ""for that route; the xDS translator emitsRouteAction_Clusterwith the per-opClusterKeydirectly. The [Proposal] Rewrite Path and Dynamic Endpoint - Resolve Conflicting Policies #1298 ext_proc + Lua architecture (x-target-upstreamheader +cluster_headerrouting) is the mechanism for upstreamDefinitions-based policy-driven dynamic endpoints. The two modes coexist: routes without a per-op override may still usecluster_headerwhenupstreamDefinitionsare present; routes with a per-op override bypass that mechanism entirely and pin to their synthetic cluster.gateway-runtimetarget_upstream_base_pathlookup and the policy-engine'sUseClusterHeader/DefaultClusterwiring from API YAML Definition with Upstream Definition and Main/Sandbox Upstreams #369 + [Proposal] Rewrite Path and Dynamic Endpoint - Resolve Conflicting Policies #1298 already route traffic correctly once the controller emits the right per-route cluster name and publishes it intoupstreamDefinitionPaths.Backward compatibility
upstreamfield is optional. Existing APIs that don't set it behave exactly as before.mainUpstream.ClusterKey/sbUpstream.ClusterKeywhen no per-op override is set for that environment, matching current behavior.UpstreamDefinitionis untouched — API-levelupstream.main/upstream.sandboxstill supportauthexactly as before. Only the new per-opOperationUpstreamTargetschema omits it.cluster_header/x-target-upstreamdynamic routing path; it sets a staticClusterKeyon the route, which the xDS translator emits as a directRouteAction_Cluster.Relevant code from #369 + #1298
For context, here are the layers that the per-cluster
basePathrewriting and dynamic cluster selection from #369 + #1298 touch:gateway-runtimepolicy-engine/internal/kernel/translator.go— theif execCtx.upstreamDefinitionPaths != nilblock that looks upupstreamDefinitionPaths[*targetUpstreamName]and setstarget_upstream_base_pathonout.DynamicMetadatafor the Lua filtergateway-runtimepolicy-engine/internal/xdsclient/handler.go— theif pathsRaw, ok := data["upstream_definition_paths"]block that hydratesrc.Metadata.UpstreamDefinitionPathsfrom xDS metadatagateway-controllerpkg/xds/translator.go—upstreamDefPaths := make(map[string]string)block that builds the lookup fromapiData.UpstreamDefinitions. Serialized into xDS metadata underupstream_definition_pathsinpkg/policyxds/snapshot.gogateway-controllerpkg/xds/translator.go— API-levelupstream.refresolution inresolveUpstreamCluster("main", ...)gateway-controllerpkg/transform/restapi.go— thefor _, op := range apiData.Operationsloop inTransform(). Per-op cluster keys are derived independently for main and sandbox vhosts.gateway-controllerpkg/xds/translator.go— fallbackfor _, op := range apiData.Operationsloop intranslateAPIConfig(). Per-op cluster derivation logic to be added here as part of this proposal (currently no per-op support; pending).platform-apiOperationRequestschema inopenapi.yaml— theOperationRequest:schema (where the new optionalupstreamfield is added) and the newOperationUpstream:/OperationUpstreamTarget:schemasBeta Was this translation helpful? Give feedback.
All reactions