Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions event-gateway/gateway-controller/cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,26 @@ func main() {
}
}

// Build the transformer registry and wire it into the Envoy translator BEFORE
// the initial xDS snapshot below, so the first snapshot already uses the
// transformer-path cluster/route names ("upstream_<name>_<host>_<port>") that the
// policy engine's resources reference. Wiring it later would leave the startup
// snapshot on the legacy naming path ("cluster_<scheme>_<host>"), breaking every
// previously deployed non-WebSub API with 503 cluster_not_found after a controller
// restart (issue #3197). WebSubApi is intentionally excluded so it keeps using the
// async-specific legacy translation path.
policyVersionResolver := utils.NewLoadedPolicyVersionResolver(policyDefinitions)
restTransformer := transform.NewRestAPITransformer(&cfg.Router, cfg, policyDefinitions)
llmTransformer := transform.NewLLMTransformer(configStore, db, &cfg.Router, cfg, policyDefinitions, policyVersionResolver)
transformerRegistry := transform.NewRegistry(restTransformer, llmTransformer)

xdsTranslator.SetTransformers(map[string]models.ConfigTransformer{
"RestApi": transformerRegistry,
"Mcp": transformerRegistry,
"LlmProvider": transformerRegistry,
"LlmProxy": transformerRegistry,
})

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
if err := snapshotManager.UpdateSnapshot(ctx, ""); err != nil {
log.Warn("Failed to generate initial xDS snapshot", slog.Any("error", err))
Expand Down Expand Up @@ -427,19 +447,10 @@ func main() {
policyManager := policyxds.NewPolicyManager(policySnapshotManager, log)
policyManager.SetRuntimeStore(runtimeStore)

policyVersionResolver := utils.NewLoadedPolicyVersionResolver(policyDefinitions)
restTransformer := transform.NewRestAPITransformer(&cfg.Router, cfg, policyDefinitions)
llmTransformer := transform.NewLLMTransformer(configStore, db, &cfg.Router, cfg, policyDefinitions, policyVersionResolver)
transformerRegistry := transform.NewRegistry(restTransformer, llmTransformer)
// Share the transformer registry (built before the initial xDS snapshot above)
// with the policy manager so both snapshot paths key resources identically.
policyManager.SetTransformers(transformerRegistry)

xdsTranslator.SetTransformers(map[string]models.ConfigTransformer{
"RestApi": transformerRegistry,
"Mcp": transformerRegistry,
"LlmProvider": transformerRegistry,
"LlmProxy": transformerRegistry,
})

loadedAPIs := configStore.GetAll()
if _, err := loadRuntimeConfigsFromExistingAPIConfigurations(loadedAPIs, runtimeStore, secretsService, transformerRegistry, log, cfg.Controller.Server.SkipInvalidDeploymentsOnStartup); err != nil {
log.Error("Failed to load runtime configs from API configurations", slog.Any("error", err))
Expand Down
49 changes: 29 additions & 20 deletions gateway/gateway-controller/cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,33 @@ func main() {
}
}

// Build transformer registry for StoredConfig → RuntimeDeployConfig conversion.
// This MUST happen before the initial xDS snapshot below: the Envoy translator
// and the policy engine must agree on cluster names ("upstream_<name>_<host>_<port>"
// from the transformer path). With no transformers wired the translator silently
// falls back to the legacy path, which names clusters "cluster_<scheme>_<host>" —
// the policy engine then routes to upstream_* clusters that don't exist in Envoy,
// and every API returns 503 cluster_not_found until it is redeployed (issue #3197).
policyVersionResolver := utils.NewLoadedPolicyVersionResolver(policyDefinitions)
restTransformer := transform.NewRestAPITransformer(&cfg.Router, cfg, policyDefinitions)
llmTransformer := transform.NewLLMTransformer(configStore, db, &cfg.Router, cfg, policyDefinitions, policyVersionResolver)
transformerRegistry := transform.NewRegistry(restTransformer, llmTransformer)

// Wire the transformer into the Envoy xDS translator so Envoy routes are built from the
// RuntimeDeployConfig (RDC) path — identical to how the policy engine's RouteConfig/PolicyChain
// resources are keyed. Without this the Envoy translator falls back to the legacy per-operation
// path, which (a) does not render header matchers and (b) names routes "method|path|vhost"
// (3 segments), while the policy resources are keyed "method|path|vhost|<header-hash>". The
// policy engine resolves the chain by the Envoy route name, so the mismatch makes every
// header-matched route fail with 500 ("policy chain not found"). WebSubApi is intentionally
// excluded so it keeps using the async-specific legacy translation path.
translator.SetTransformers(map[string]models.ConfigTransformer{
"RestApi": transformerRegistry,
"Mcp": transformerRegistry,
"LlmProvider": transformerRegistry,
"LlmProxy": transformerRegistry,
})

// Generate initial xDS snapshot
log.Info("Generating initial xDS snapshot")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
Expand Down Expand Up @@ -426,28 +453,10 @@ func main() {
policyManager := policyxds.NewPolicyManager(policySnapshotManager, log)
policyManager.SetRuntimeStore(runtimeStore)

// Build transformer registry for StoredConfig → RuntimeDeployConfig conversion
policyVersionResolver := utils.NewLoadedPolicyVersionResolver(policyDefinitions)
restTransformer := transform.NewRestAPITransformer(&cfg.Router, cfg, policyDefinitions)
llmTransformer := transform.NewLLMTransformer(configStore, db, &cfg.Router, cfg, policyDefinitions, policyVersionResolver)
transformerRegistry := transform.NewRegistry(restTransformer, llmTransformer)
// Share the transformer registry (built before the initial xDS snapshot above)
// with the policy manager so both snapshot paths key resources identically.
policyManager.SetTransformers(transformerRegistry)

// Wire the same transformer into the Envoy xDS translator so Envoy routes are built from the
// RuntimeDeployConfig (RDC) path — identical to how the policy engine's RouteConfig/PolicyChain
// resources are keyed. Without this the Envoy translator falls back to the legacy per-operation
// path, which (a) does not render header matchers and (b) names routes "method|path|vhost"
// (3 segments), while the policy resources are keyed "method|path|vhost|<header-hash>". The
// policy engine resolves the chain by the Envoy route name, so the mismatch makes every
// header-matched route fail with 500 ("policy chain not found"). WebSubApi is intentionally
// excluded so it keeps using the async-specific legacy translation path.
translator.SetTransformers(map[string]models.ConfigTransformer{
"RestApi": transformerRegistry,
"Mcp": transformerRegistry,
"LlmProvider": transformerRegistry,
"LlmProxy": transformerRegistry,
})

// Load runtime configs from existing API configurations on startup.
// We write directly to runtimeStore to avoid triggering N separate snapshot updates;
// the single UpdateSnapshot call below covers all of them.
Expand Down
12 changes: 11 additions & 1 deletion gateway/gateway-controller/pkg/xds/translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,17 @@ func (t *Translator) TranslateConfigs(
var err error

// Try RuntimeDeployConfig transformer path first (produces minimal metadata routes)
if transformer, ok := t.transformers[cfg.Kind]; ok {
transformer, ok := t.transformers[cfg.Kind]
if !ok && cfg.Kind != "WebSubApi" {
// Transformers should be registered for every non-WebSub kind before the
// first snapshot is generated. Falling back here means Envoy cluster/route
// names will not match the policy engine's resources (503 cluster_not_found /
// 500 policy chain not found) — see issue #3197.
log.Warn("No transformer registered for config kind, using legacy translation path",
slog.String("id", cfg.UUID),
slog.String("kind", cfg.Kind))
}
if ok {
rdc, transformErr := transformer.Transform(cfg)
if transformErr != nil {
log.Error("Failed to transform config via RuntimeDeployConfig, falling back to legacy path",
Expand Down
Loading