Skip to content

Commit 8539b53

Browse files
fix(gateway): add attached route counts and ResolvedRefs to listener status
Gateway reconciler improvements for conformance: - Count actual attached HTTPRoutes per listener by querying all HTTPRoutes and checking parentRefs/sectionName - Add ResolvedRefs=True condition to all listener statuses - Requeue Gateway every 10s to pick up route count changes (HTTPRoutes are separate resources, don't trigger Gateway watch) - Remove generation-based skip for Gateway (route counts can change without Gateway generation changing) Conformance results (6 passed, 11 failed, 1 timeout): - PASS: GatewayModifyListeners, GatewaySecretReferenceGrant*, GatewayWithAttachedRoutes (route counting works) - FAIL: InvalidRouteKind, InvalidTLS, MissingReferenceGrant (controller always reports True, needs negative-case handling) - TIMEOUT: HTTPRoute tests wait for Gateway address in status (controller doesn't set addresses from LoadBalancer Service yet)
1 parent b607eb7 commit 8539b53

1 file changed

Lines changed: 65 additions & 37 deletions

File tree

crates/gateway/src/reconcilers/gateway.rs

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
66
use gateway_api::gatewayclasses::GatewayClass;
77
use gateway_api::gateways::Gateway;
8-
use kube::api::{Api, Patch, PatchParams};
8+
use gateway_api::httproutes::HTTPRoute;
9+
use kube::api::{Api, ListParams, Patch, PatchParams};
910
use kube::runtime::controller::Action;
1011
use kube::{Client, ResourceExt};
1112
use serde_json::json;
@@ -50,16 +51,10 @@ impl GatewayReconciler {
5051

5152
let generation = gateway.metadata.generation.unwrap_or(0);
5253

53-
// Skip if we've already programmed this generation
54-
if is_already_programmed(&gateway, generation) {
55-
debug!(
56-
name = %name,
57-
namespace = %namespace,
58-
generation,
59-
"Gateway already programmed at this generation"
60-
);
61-
return Ok(Action::await_change());
62-
}
54+
// Note: we do NOT skip based on generation alone here because
55+
// attached route counts can change without the Gateway's generation
56+
// changing (HTTPRoutes are separate resources). We always recompute
57+
// status but use a requeue interval to avoid tight loops.
6358

6459
info!(
6560
name = %name,
@@ -69,10 +64,13 @@ impl GatewayReconciler {
6964
"Reconciling Gateway"
7065
);
7166

72-
// Update Gateway status
67+
// Update Gateway status with listener info and attached route counts
7368
self.update_status(&gateway, &namespace).await?;
7469

75-
Ok(Action::await_change())
70+
// Requeue periodically to pick up changes in attached route counts
71+
// (HTTPRoutes are separate resources, their changes don't trigger
72+
// Gateway reconciliation directly)
73+
Ok(Action::requeue(std::time::Duration::from_secs(10)))
7674
}
7775

7876
/// Check if a GatewayClass name belongs to our controller.
@@ -88,6 +86,41 @@ impl GatewayReconciler {
8886
}
8987
}
9088

89+
/// Count attached HTTPRoutes per listener for this Gateway.
90+
async fn count_attached_routes(
91+
&self,
92+
gw_name: &str,
93+
gw_namespace: &str,
94+
) -> Result<std::collections::HashMap<String, i32>, GatewayError> {
95+
let route_api: Api<HTTPRoute> = Api::all(self.client.clone());
96+
let routes = route_api.list(&ListParams::default()).await?;
97+
98+
let mut counts: std::collections::HashMap<String, i32> =
99+
std::collections::HashMap::new();
100+
101+
for route in &routes.items {
102+
let parent_refs = route.spec.parent_refs.as_ref();
103+
if let Some(refs) = parent_refs {
104+
for pr in refs {
105+
let route_ns = route.namespace().unwrap_or_default();
106+
let pr_ns = pr.namespace.as_deref().unwrap_or(&route_ns);
107+
if pr.name == gw_name && pr_ns == gw_namespace {
108+
// If sectionName is specified, count for that listener
109+
// Otherwise, count for all listeners
110+
if let Some(ref section) = pr.section_name {
111+
*counts.entry(section.clone()).or_insert(0) += 1;
112+
} else {
113+
// Attach to all listeners (use empty string as wildcard key)
114+
*counts.entry(String::new()).or_insert(0) += 1;
115+
}
116+
}
117+
}
118+
}
119+
}
120+
121+
Ok(counts)
122+
}
123+
91124
/// Update Gateway status conditions and addresses.
92125
async fn update_status(
93126
&self,
@@ -98,15 +131,25 @@ impl GatewayReconciler {
98131
let generation = gateway.metadata.generation.unwrap_or(0);
99132
let now = chrono::Utc::now().to_rfc3339();
100133

134+
// Count attached routes per listener
135+
let attached_counts = self.count_attached_routes(&name, namespace).await?;
136+
let wildcard_count = attached_counts.get("").copied().unwrap_or(0);
137+
101138
// Build listener statuses
102139
let listener_statuses: Vec<serde_json::Value> = gateway
103140
.spec
104141
.listeners
105142
.iter()
106143
.map(|l| {
144+
let listener_count = attached_counts
145+
.get(&l.name)
146+
.copied()
147+
.unwrap_or(0)
148+
+ wildcard_count;
149+
107150
json!({
108151
"name": l.name,
109-
"attachedRoutes": 0,
152+
"attachedRoutes": listener_count,
110153
"supportedKinds": supported_route_kinds(&l.protocol),
111154
"conditions": [{
112155
"type": "Accepted",
@@ -122,6 +165,13 @@ impl GatewayReconciler {
122165
"message": "Listener programmed in Zentinel",
123166
"observedGeneration": generation,
124167
"lastTransitionTime": now,
168+
}, {
169+
"type": "ResolvedRefs",
170+
"status": "True",
171+
"reason": "ResolvedRefs",
172+
"message": "All references resolved",
173+
"observedGeneration": generation,
174+
"lastTransitionTime": now,
125175
}]
126176
})
127177
})
@@ -140,7 +190,7 @@ impl GatewayReconciler {
140190
"type": "Programmed",
141191
"status": "True",
142192
"reason": "Programmed",
143-
"message": "Gateway programmed listeners active",
193+
"message": "Gateway programmed, listeners active",
144194
"observedGeneration": generation,
145195
"lastTransitionTime": now,
146196
}],
@@ -170,28 +220,6 @@ impl GatewayReconciler {
170220
}
171221
}
172222

173-
/// Check if the Gateway status already has Accepted=True and Programmed=True
174-
/// for the current generation.
175-
fn is_already_programmed(gw: &Gateway, generation: i64) -> bool {
176-
let Some(ref status) = gw.status else {
177-
return false;
178-
};
179-
let Some(ref conditions) = status.conditions else {
180-
return false;
181-
};
182-
let has_accepted = conditions.iter().any(|c| {
183-
c.type_ == "Accepted"
184-
&& c.status == "True"
185-
&& c.observed_generation == Some(generation)
186-
});
187-
let has_programmed = conditions.iter().any(|c| {
188-
c.type_ == "Programmed"
189-
&& c.status == "True"
190-
&& c.observed_generation == Some(generation)
191-
});
192-
has_accepted && has_programmed
193-
}
194-
195223
/// Return the supported route kinds for a given listener protocol.
196224
fn supported_route_kinds(protocol: &str) -> Vec<serde_json::Value> {
197225
match protocol {

0 commit comments

Comments
 (0)