Skip to content

Commit 9c8c266

Browse files
committed
Refactor custom policy chain extraction logic
1 parent 4bbe676 commit 9c8c266

26 files changed

Lines changed: 2063 additions & 2208 deletions

gateway/gateway-controller/pkg/models/runtime_deploy_config.go

Lines changed: 26 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -91,32 +91,24 @@ type Route struct {
9191
// CanonicalChainKey is the key of the policy chain this route's requests use
9292
// when the operation is determined by the route itself. It equals the route key
9393
// for every kind shipping today, but it is emitted as its own explicit field
94-
// rather than left implicit, because that is what lets an identity route be
95-
// pointed at a *composed* operation key without a wire change — which is exactly
96-
// what an A2A HTTP+JSON operation route does, one route per operation path.
97-
// Empty means "same as the route key".
94+
// rather than left implicit, because that is what lets a directly-resolved route be
95+
// pointed at a *composed* operation key without a wire change. Empty means "same as
96+
// the route key".
9897
//
99-
// A resolver-bearing route must leave this empty: it has no chain of its own, its
100-
// chain being composed per request from the operation the resolver identifies.
98+
// A route naming a protocol resolver must leave this empty. Its key is derived from
99+
// its own ResolverConfig by the resolver that owns the protocol, so a key here would
100+
// be a second copy of the same fact with nothing to arbitrate between them — see
101+
// ValidateResolution, which rejects the combination.
101102
CanonicalChainKey string
102103

103104
// ResolverName overrides RuntimeDeployConfig.PolicyChainResolver for this one
104-
// route. It exists because a multiplexed API has both kinds of route at once: an
105-
// A2A deployment serves identity HTTP+JSON routes and a body-resolved JSON-RPC
106-
// route side by side. Empty inherits the RDC-level default.
105+
// route. It exists because one API can hold both shapes at once: routes whose
106+
// operation is only knowable from the request name a protocol resolver, while the
107+
// routes that are not operations at all — a well-known metadata document, a CORS
108+
// preflight — stay directly resolved beside them. Empty inherits the RDC-level
109+
// default.
107110
ResolverName string
108111

109-
// ResponseKind is how this route's operation delivers its response: "unary",
110-
// "streaming", or empty for "the policy engine derives it as it always has".
111-
//
112-
// It exists because buffering a streaming response is not a slow path but a broken
113-
// one — the caller gets nothing until the upstream closes, which for a long-running
114-
// agent task may be never — and the policy chain alone cannot tell the engine which
115-
// it is. Set it on a route whose operation is known at deploy time (one A2A HTTP+JSON
116-
// path per operation). On a multiplexed route leave it empty: both kinds of operation
117-
// arrive on the same route, so only the resolver can say which this request is.
118-
ResponseKind string
119-
120112
// ResolverConfig is opaque, resolver-specific per-route configuration. The policy
121113
// engine passes it to the resolver's Prepare hook once at xDS ingest, so a
122114
// resolver that must compile a schema or build an index does it there rather than
@@ -197,18 +189,6 @@ type ConfigTransformer interface {
197189
Transform(cfg *StoredConfig) (*RuntimeDeployConfig, error)
198190
}
199191

200-
// Response kinds a route may declare. They must match the policy engine's
201-
// resolver.ResponseKind values; the two are separate declarations because the controller
202-
// and the runtime are separate modules agreeing on a wire value, and a mismatch here is
203-
// caught loudly — the engine ignores a kind it does not recognise and logs it, and
204-
// ValidateResolution refuses to emit one in the first place.
205-
const (
206-
ResponseKindUnary = "unary"
207-
ResponseKindStreaming = "streaming"
208-
)
209-
210-
var validResponseKinds = map[string]bool{"": true, ResponseKindUnary: true, ResponseKindStreaming: true}
211-
212192
// RouteKeyResolverName is the resolver name meaning "the route determines the
213193
// operation" — the identity case. It must match the policy engine's
214194
// resolver.RouteKeyResolverName; the two are separate constants because the
@@ -234,10 +214,14 @@ func (rdc *RuntimeDeployConfig) EffectiveCanonicalChainKey(routeKey string, rout
234214
return routeKey
235215
}
236216

237-
// isIdentityResolver reports whether a resolver name means "the route determines the
238-
// operation". An empty name does, because that is what every RDC looked like before
239-
// resolvers existed.
240-
func isIdentityResolver(name string) bool {
217+
// IsDirectlyResolved reports whether a resolver name means "the route itself determines
218+
// the chain key", so the route carries that key rather than deriving one per request. An
219+
// empty name does, because that is what every RDC looked like before resolvers existed.
220+
//
221+
// Exported because the snapshot translator decides from it whether to put
222+
// canonical_chain_key on the wire at all, and that decision has to agree with the
223+
// validation rule below — two predicates could disagree.
224+
func IsDirectlyResolved(name string) bool {
241225
return name == "" || name == RouteKeyResolverName
242226
}
243227

@@ -255,8 +239,8 @@ func isIdentityResolver(name string) bool {
255239
// from "the map points at a missing chain" to "a key the engine will compose has no
256240
// chain", so the checks moved with it:
257241
//
258-
// - an identity route's canonical key must name a chain (this covers a redirected
259-
// HTTP+JSON operation route, whose composed key must resolve like any other);
242+
// - a directly-resolved route's canonical key must name a chain (including one pointed
243+
// at a composed operation key, whose key must resolve like any other);
260244
// - a resolver-bearing route must not carry a canonical key, and must have at least
261245
// one operation chain in its own partition — otherwise no request to it can ever
262246
// resolve;
@@ -300,21 +284,16 @@ func (rdc *RuntimeDeployConfig) ValidateResolution() error {
300284
return fmt.Errorf("route %q: nil route", routeKey)
301285
}
302286

303-
if !validResponseKinds[route.ResponseKind] {
304-
return fmt.Errorf("route %q: unknown response kind %q (expected %q, %q, or empty)",
305-
routeKey, route.ResponseKind, ResponseKindUnary, ResponseKindStreaming)
306-
}
307-
308287
resolverName := rdc.EffectiveResolverName(route)
309288

310-
if isIdentityResolver(resolverName) {
289+
if IsDirectlyResolved(resolverName) {
311290
canonical := rdc.EffectiveCanonicalChainKey(routeKey, route)
312291
if _, ok := rdc.PolicyChains[canonical]; !ok {
313292
return fmt.Errorf("route %q: canonical chain key %q names no policy chain", routeKey, canonical)
314293
}
315-
// Existing is not enough. A redirected identity route (an A2A HTTP+JSON
316-
// operation route) points at a composed key, and a chain composed for another
317-
// routing partition is a perfectly valid chain that belongs to someone else:
294+
// Existing is not enough. A directly-resolved route may be pointed at a
295+
// composed operation key, and a chain composed for another routing partition
296+
// is a perfectly valid chain that belongs to someone else:
318297
// a production route pointed at a sandbox operation chain would run the
319298
// sandbox's authentication, authorization and rate limits. Existence checks
320299
// catch a missing chain; only this catches the wrong one.

0 commit comments

Comments
 (0)