Skip to main content

rws_gateway_api/gateway_api/
translate.rs

1//! The pure step of the controller: a [`Snapshot`] of the cluster in, the
2//! configuration rws serves ([`GatewayConfig`]) and the status every object
3//! should have ([`Desired`]) out. No I/O, so the Gateway API's acceptance
4//! rules — which routes attach to which listeners, which references resolve,
5//! which conditions and reasons are reported — are unit-tested here.
6
7use std::collections::{BTreeMap, BTreeSet};
8
9use k8s_openapi::api::core::v1::{ConfigMap, Namespace, Pod, Secret, Service};
10use k8s_openapi::api::discovery::v1::EndpointSlice;
11use k8s_openapi::apimachinery::pkg::apis::meta::v1::{Condition, LabelSelector, Time};
12use kube::ResourceExt;
13
14use super::resources::{BackendTlsPolicy, Gateway, GatewayClass, GrpcRoute, HttpRoute, ListenerSet, ReferenceGrant, TcpRoute, TlsRoute, UdpRoute};
15use super::routes::CompiledHttpRoute;
16use super::services::ServiceSettings;
17use super::types::*;
18
19/// What translation needs besides the cluster.
20#[derive(Clone, Debug)]
21pub struct Settings {
22    pub controller_name: String,
23    pub services: ServiceSettings,
24    /// Provisioned mode: a Deployment per Gateway.
25    pub provision: Option<super::provision::ProvisionSettings>,
26    /// The mesh profile: routes attached to Services are reported on.
27    pub mesh: bool,
28    /// The experimental-channel features (the cluster has the experimental
29    /// CRDs): address routability, listener filters, ClusterTrustBundle.
30    /// Their status fields would be pruned by the standard CRDs.
31    pub experimental: bool,
32}
33
34impl Default for Settings {
35    fn default() -> Self {
36        Settings { controller_name: super::CONTROLLER_NAME.to_string(), services: ServiceSettings::default(), provision: None, mesh: false, experimental: false }
37    }
38}
39
40/// Everything the controller watches, as last seen.
41#[derive(Clone, Default)]
42pub struct Snapshot {
43    pub classes: Vec<GatewayClass>,
44    pub gateways: Vec<Gateway>,
45    pub http_routes: Vec<HttpRoute>,
46    pub grpc_routes: Vec<GrpcRoute>,
47    pub tls_routes: Vec<TlsRoute>,
48    pub tcp_routes: Vec<TcpRoute>,
49    pub udp_routes: Vec<UdpRoute>,
50    pub listener_sets: Vec<ListenerSet>,
51    pub backend_tls_policies: Vec<BackendTlsPolicy>,
52    pub reference_grants: Vec<ReferenceGrant>,
53    pub services: Vec<Service>,
54    pub namespaces: Vec<Namespace>,
55    pub secrets: Vec<Secret>,
56    /// For client-certificate CAs (`spec.tls.frontend`).
57    pub config_maps: Vec<ConfigMap>,
58    pub endpoint_slices: Vec<EndpointSlice>,
59    /// rws's own serving pods (in the pod namespace, by the pod selector).
60    pub pods: Vec<Pod>,
61    /// Provisioned Deployments, in the pod namespace.
62    pub deployments: Vec<k8s_openapi::api::apps::v1::Deployment>,
63    /// Whether the cluster serves ClusterTrustBundles (`certificates.k8s.io`).
64    pub cluster_trust_bundles_served: bool,
65    /// ClusterTrustBundles: (name, `spec.trustBundle` PEM).
66    pub cluster_trust_bundles: Vec<(String, String)>,
67}
68
69/// The label rws puts on the Service it creates for a Gateway (the name the
70/// Gateway API recommends), next to [`MANAGED_BY`].
71pub const GATEWAY_NAME_LABEL: &str = "gateway.networking.k8s.io/gateway-name";
72/// `app.kubernetes.io/managed-by` on everything the controller creates.
73pub const MANAGED_BY: &str = "rws-gateway";
74
75/// A Gateway's addresses: its Service's load-balancer ingress.
76pub fn gateway_addresses(snapshot: &Snapshot, namespace: &str, name: &str) -> Vec<GatewayStatusAddress> {
77    let service = snapshot.services.iter().find(|s| {
78        s.namespace().as_deref() == Some(namespace)
79            && s.labels().get(GATEWAY_NAME_LABEL).map(String::as_str) == Some(name)
80            && s.labels().get("app.kubernetes.io/managed-by").map(String::as_str) == Some(MANAGED_BY)
81    });
82    let ingress = service.and_then(|s| s.status.as_ref()).and_then(|s| s.load_balancer.as_ref()).and_then(|lb| lb.ingress.clone()).unwrap_or_default();
83    ingress
84        .into_iter()
85        .filter_map(|i| match (i.ip, i.hostname) {
86            (Some(ip), _) => Some(GatewayStatusAddress { address_type: Some("IPAddress".to_string()), value: ip, routability: None }),
87            (None, Some(hostname)) => Some(GatewayStatusAddress { address_type: Some("Hostname".to_string()), value: hostname, routability: None }),
88            _ => None,
89        })
90        .collect()
91}
92
93/// The status each object should have; written only where it differs.
94#[derive(Clone, Debug, Default, PartialEq)]
95pub struct Desired {
96    /// GatewayClass name → status.
97    pub classes: BTreeMap<String, GatewayClassStatus>,
98    /// (namespace, name) → status.
99    pub gateways: BTreeMap<(String, String), GatewayStatus>,
100    /// (namespace, name) → this controller's parent statuses (other
101    /// controllers' entries are kept when written).
102    pub http_routes: BTreeMap<(String, String), Vec<RouteParentStatus>>,
103    /// The same for GRPCRoutes.
104    pub grpc_routes: BTreeMap<(String, String), Vec<RouteParentStatus>>,
105    pub tls_routes: BTreeMap<(String, String), Vec<RouteParentStatus>>,
106    pub tcp_routes: BTreeMap<(String, String), Vec<RouteParentStatus>>,
107    pub udp_routes: BTreeMap<(String, String), Vec<RouteParentStatus>>,
108    /// (namespace, name) → status of the ListenerSets attached to our Gateways.
109    pub listener_sets: BTreeMap<(String, String), ListenerSetStatus>,
110    /// (namespace, name) → this controller's ancestor statuses of each BackendTLSPolicy.
111    pub backend_tls_policies: BTreeMap<(String, String), Vec<PolicyAncestorStatus>>,
112    /// (namespace, name) → the Service each of our Gateways should have.
113    pub services: BTreeMap<(String, String), Service>,
114    /// (namespace, name) → those Services' EndpointSlices (rws's pods).
115    pub endpoint_slices: BTreeMap<(String, String), EndpointSlice>,
116    /// Provisioned mode: (Gateway namespace, name) → its Deployment.
117    pub deployments: BTreeMap<(String, String), k8s_openapi::api::apps::v1::Deployment>,
118}
119
120/// What the data plane serves.
121#[derive(Clone, Debug, Default, PartialEq)]
122pub struct GatewayConfig {
123    pub listeners: Vec<ListenerConfig>,
124}
125
126/// One accepted listener, the container port it's served on, and the
127/// routes attached to it. Only listeners whose Gateway's Service already maps
128/// their port are served, so every replica agrees on the port.
129#[derive(Clone, Debug, PartialEq)]
130pub struct ListenerConfig {
131    pub gateway: (String, String),
132    pub name: String,
133    pub port: i32,
134    pub protocol: String,
135    pub hostname: Option<String>,
136    pub container_port: u16,
137    /// PEM certificate chain and key, for HTTPS.
138    pub tls: Option<(Vec<u8>, Vec<u8>)>,
139    /// Client certificates to ask for (`spec.tls.frontend` for its port).
140    pub client_ca: Option<ClientCa>,
141    /// The certificate (PEM chain, key) its Gateway presents to TLS
142    /// backends (`spec.tls.backend.clientCertificateRef`).
143    pub backend_client_cert: Option<(Vec<u8>, Vec<u8>)>,
144    /// Pre-routing filters (`filters.requests`, GEP-5224), in order.
145    pub request_filters: Vec<super::ext_auth::CompiledAuth>,
146    pub http_routes: Vec<CompiledHttpRoute>,
147}
148
149/// The CAs a TLS-terminating listener verifies client certificates with.
150#[derive(Clone, Debug, PartialEq)]
151pub struct ClientCa {
152    /// PEM certificates, from the `ca.crt` of each referenced ConfigMap.
153    pub pem: Vec<u8>,
154    /// `AllowInsecureFallback`: ask, but let in clients without a valid one.
155    pub insecure_fallback: bool,
156}
157
158/// The features rws reports on its GatewayClasses (names as in the suite's
159/// `pkg/features` at Gateway API v1.7.0; sorted). Grows with each phase;
160/// `FEATURES` in `.github/workflows/gateway-api.yml` must match.
161/// The experimental-channel features rws claims with
162/// `RWS_CONFIG_GATEWAY_API_EXPERIMENTAL` (sorted); `EXPERIMENTAL_FEATURES`
163/// in `.github/workflows/gateway-api.yml` must match.
164pub const EXPERIMENTAL_FEATURES: &[&str] = &["ClusterTrustBundle", "GatewayAddressRoutability", "GatewayAddressRoutabilityCluster", "GatewayListenerFilters", "HTTPRouteDestinationPortMatching"];
165
166pub const SUPPORTED_FEATURES: &[&str] = &[
167    "BackendTLSPolicy",
168    "BackendTLSPolicySANValidation",
169    "GRPCRoute",
170    "GRPCRouteNamedRouteRule",
171    "GRPCRouteRequestHeaderModifier",
172    "Gateway",
173    "GatewayAddressEmpty",
174    "GatewayBackendClientCertificate",
175    "GatewayFrontendClientCertificateValidation",
176    "GatewayFrontendClientCertificateValidationInsecureFallback",
177    "GatewayHTTPListenerIsolation",
178    "GatewayHTTPSListenerDetectMisdirectedRequests",
179    "GatewayInfrastructure",
180    "GatewayPort8080",
181    "GatewayRouteHostnameIntersectionPrecedence",
182    "GatewayStaticAddresses",
183    "HTTPRoute",
184    "HTTPRoute303RedirectStatusCode",
185    "HTTPRoute307RedirectStatusCode",
186    "HTTPRoute308RedirectStatusCode",
187    "HTTPRouteBackendProtocolH2C",
188    "HTTPRouteBackendProtocolWebSocket",
189    "HTTPRouteBackendRequestHeaderModification",
190    "HTTPRouteBackendTimeout",
191    "HTTPRouteBackendURLRewrite",
192    "HTTPRouteCORS",
193    "HTTPRouteHostRewrite",
194    "HTTPRouteMethodMatching",
195    "HTTPRouteNamedRouteRule",
196    "HTTPRouteParentRefPort",
197    "HTTPRoutePathRedirect",
198    "HTTPRoutePathRewrite",
199    "HTTPRoutePortRedirect",
200    "HTTPRouteQueryParamMatching",
201    "HTTPRouteRequestMirror",
202    "HTTPRouteRequestMultipleMirrors",
203    "HTTPRouteRequestPercentageMirror",
204    "HTTPRouteRequestTimeout",
205    "HTTPRouteResponseHeaderModification",
206    "HTTPRouteRetryCodes",
207    "HTTPRouteRetryConnectionError",
208    "HTTPRouteSchemeRedirect",
209    "ListenerSet",
210    "ReferenceGrant",
211    "TCPRoute",
212    "TLSRoute",
213    "TLSRouteModeMixed",
214    "TLSRouteModeTerminate",
215    "UDPRoute",
216];
217
218pub fn translate(snapshot: &Snapshot, settings: &Settings, now: &Time) -> (GatewayConfig, Desired) {
219    let controller_name = settings.controller_name.as_str();
220    let mut desired = Desired::default();
221    let mut config = GatewayConfig::default();
222
223    // GatewayClasses: ours are accepted.
224    let mut our_classes = BTreeSet::new();
225    for class in &snapshot.classes {
226        if class.spec.controller_name != controller_name {
227            continue;
228        }
229        let name = class.name_any();
230        our_classes.insert(name.clone());
231        let previous = class.status.as_ref().map(|s| s.conditions.as_slice()).unwrap_or_default();
232        let generation = class.metadata.generation;
233        let conditions = vec![condition(previous, "Accepted", true, "Accepted", "Handled by rws", generation, now)];
234        let mut names: Vec<&str> = SUPPORTED_FEATURES.to_vec();
235        if settings.experimental {
236            // ClusterTrustBundle only where the cluster serves the API (GEP-5075).
237            names.extend(EXPERIMENTAL_FEATURES.iter().filter(|f| **f != "ClusterTrustBundle" || snapshot.cluster_trust_bundles_served));
238            names.sort();
239        }
240        let supported_features = names.into_iter().map(|f| SupportedFeature { name: f.to_string() }).collect();
241        desired.classes.insert(name, GatewayClassStatus { conditions, supported_features });
242    }
243
244    // Gateways of our classes, and what each listener accepts: their own,
245    // then those of the ListenerSets they allow (oldest first).
246    let mut listeners: Vec<ListenerView> = Vec::new();
247    let mut listener_sets: Vec<(&ListenerSet, (String, String), bool)> = Vec::new();
248    let mut merged_gateways: Vec<Gateway> = Vec::new();
249    for gateway in &snapshot.gateways {
250        if !our_classes.contains(&gateway.spec.gateway_class_name) {
251            continue;
252        }
253        let key = (gateway.namespace().unwrap_or_default(), gateway.name_any());
254        let mut views = listener_views(gateway, &gateway.spec.listeners, &key.0, None, snapshot);
255        let mut merged = gateway.clone();
256        for (set, allowed) in attached_listener_sets(gateway, snapshot) {
257            let set_key = (set.namespace().unwrap_or_default(), set.name_any());
258            if allowed {
259                views.extend(listener_views(gateway, &set.spec.listeners, &set_key.0, Some(set_key.clone()), snapshot));
260                merged.spec.listeners.extend(set.spec.listeners.iter().cloned());
261            }
262            listener_sets.push((set, key.clone(), allowed));
263        }
264        mark_conflicts(&mut views);
265        listeners.extend(views);
266        merged_gateways.push(merged);
267    }
268
269    // Every Gateway of ours gets its Service, with a port for every listener
270    // (its ListenerSets' too).
271    let our_gateways: Vec<&Gateway> = merged_gateways.iter().collect();
272    desired.services = super::services::desired(snapshot, &our_gateways, &settings.services);
273    desired.endpoint_slices = super::services::endpoint_slices(snapshot, &desired.services, &settings.services);
274    if let Some(provision) = &settings.provision {
275        let gateways: Vec<&Gateway> = snapshot.gateways.iter().filter(|g| our_classes.contains(&g.spec.gateway_class_name)).collect();
276        desired.deployments = super::provision::desired(&gateways, &settings.services.pod_namespace, controller_name, settings.services.port_range, provision);
277    }
278
279    // Routes: attach to listeners, resolve backends.
280    let routes = snapshot
281        .http_routes
282        .iter()
283        .map(|r| RouteIn::new("HTTPRoute", r, &r.spec.parent_refs, &r.spec.hostnames, http_references(&r.spec.rules), super::routes::compile(r, snapshot)))
284        .chain(snapshot.grpc_routes.iter().map(|r| RouteIn::new("GRPCRoute", r, &r.spec.parent_refs, &r.spec.hostnames, grpc_references(&r.spec.rules), super::routes::compile_grpc(r, snapshot))))
285        .chain([("TLSRoute", &snapshot.tls_routes), ("TCPRoute", &snapshot.tcp_routes), ("UDPRoute", &snapshot.udp_routes)].into_iter().flat_map(|(kind, routes)| {
286            routes.iter().map(move |r| RouteIn::new(kind, r, &r.spec.parent_refs, &r.spec.hostnames, l4_references(&r.spec.rules), super::routes::compile_l4(r, kind, snapshot)))
287        }));
288    let mut attached: BTreeMap<ListenerKey, i32> = BTreeMap::new();
289    // (Service namespace, name) → Gateways with an attached route to it.
290    let mut service_gateways: BTreeMap<(String, String), BTreeSet<(String, String)>> = BTreeMap::new();
291    for route in routes {
292        let namespace = route.namespace.clone();
293        let resolved = resolve_references(route.kind, &namespace, &route.references, snapshot);
294        let mut parents = Vec::new();
295        for parent_ref in route.parent_refs {
296            // A Service parent: the mesh profile ([`super::mesh`]).
297            if settings.mesh && matches!(route.kind, "HTTPRoute" | "GRPCRoute") && super::mesh::is_service_parent(parent_ref) {
298                let old = route
299                    .previous
300                    .iter()
301                    .find(|p| p.controller_name == controller_name && same_parent(&p.parent_ref, parent_ref, &namespace))
302                    .map(|p| p.conditions.as_slice())
303                    .unwrap_or_default();
304                let (accepted, reason, message) = super::mesh::parent_acceptance(parent_ref, &namespace, &snapshot.services);
305                // A consumer route may send to its parent Service in another
306                // namespace without a ReferenceGrant (GAMMA).
307                let resolved = match &resolved {
308                    Err(("RefNotPermitted", _)) if super::mesh::only_parent_references(&route.references, route.parent_refs, &namespace) => {
309                        let local: Vec<&BackendObjectReference> = route.references.iter().filter(|r| r.namespace.as_deref().is_none_or(|n| n == namespace)).copied().collect();
310                        resolve_references(route.kind, &namespace, &local, snapshot)
311                    }
312                    other => other.clone(),
313                };
314                let conditions = vec![
315                    condition(old, "Accepted", accepted, reason, &message, route.generation, now),
316                    match &resolved {
317                        Ok(()) => condition(old, "ResolvedRefs", true, "ResolvedRefs", "All references resolved", route.generation, now),
318                        Err((reason, message)) => condition(old, "ResolvedRefs", false, reason, message, route.generation, now),
319                    },
320                ];
321                parents.push(RouteParentStatus { parent_ref: parent_ref.clone(), controller_name: controller_name.to_string(), conditions });
322                continue;
323            }
324            if parent_ref.group() != GATEWAY_GROUP {
325                continue;
326            }
327            let parent_key = (parent_ref.namespace.clone().unwrap_or_else(|| namespace.clone()), parent_ref.name.clone());
328            // The Gateway, and for a ListenerSet parent the set whose listeners it means.
329            let (gateway_key, owner) = match parent_ref.kind() {
330                "Gateway" => (parent_key.clone(), None),
331                "ListenerSet" => match listener_sets.iter().find(|(set, _, _)| is_named(*set, &parent_key)) {
332                    Some((_, gateway, _)) => (gateway.clone(), Some(parent_key.clone())),
333                    None => continue, // not attached to a Gateway of ours
334                },
335                _ => continue,
336            };
337            let candidates: Vec<&ListenerView> = listeners.iter().filter(|l| l.gateway == gateway_key && l.owner == owner).collect();
338            if candidates.is_empty() && !snapshot.gateways.iter().any(|g| is_named(g, &gateway_key) && our_classes.contains(&g.spec.gateway_class_name)) {
339                continue; // not one of ours: another controller reports it
340            }
341            let old = route
342                .previous
343                .iter()
344                .find(|p| p.controller_name == controller_name && same_parent(&p.parent_ref, parent_ref, &namespace))
345                .map(|p| p.conditions.as_slice())
346                .unwrap_or_default();
347            let (accepted, reason, message, attached_to) = attach(route.kind, route.hostnames, parent_ref, &namespace, &candidates, snapshot);
348            if !attached_to.is_empty() {
349                for reference in &route.references {
350                    let service = (reference.namespace.clone().unwrap_or_else(|| namespace.clone()), reference.name.clone());
351                    service_gateways.entry(service).or_default().insert(gateway_key.clone());
352                }
353            }
354            for listener in &attached_to {
355                *attached.entry((gateway_key.clone(), owner.clone(), listener.clone())).or_default() += 1;
356                if let Some(view) = listeners.iter_mut().find(|l| l.gateway == gateway_key && l.owner == owner && &l.name == listener) {
357                    view.http_routes.push(route.compiled.clone());
358                }
359            }
360            let conditions = vec![
361                condition(old, "Accepted", accepted, reason, &message, route.generation, now),
362                match &resolved {
363                    Ok(()) => condition(old, "ResolvedRefs", true, "ResolvedRefs", "All references resolved", route.generation, now),
364                    Err((reason, message)) => condition(old, "ResolvedRefs", false, reason, message, route.generation, now),
365                },
366            ];
367            parents.push(RouteParentStatus { parent_ref: parent_ref.clone(), controller_name: controller_name.to_string(), conditions });
368        }
369        if !parents.is_empty() {
370            let statuses = match route.kind {
371                "GRPCRoute" => &mut desired.grpc_routes,
372                "TLSRoute" => &mut desired.tls_routes,
373                "TCPRoute" => &mut desired.tcp_routes,
374                "UDPRoute" => &mut desired.udp_routes,
375                _ => &mut desired.http_routes,
376            };
377            statuses.insert((namespace, route.name), parents);
378        }
379    }
380
381    // BackendTLSPolicies: status per Gateway whose routes reach a target.
382    for ((namespace, name), evaluated) in super::backend_tls::evaluate(snapshot) {
383        let Some(policy) = snapshot.backend_tls_policies.iter().find(|p| p.namespace().as_deref() == Some(namespace.as_str()) && p.name_any() == name) else { continue };
384        let generation = policy.metadata.generation;
385        let gateways: BTreeSet<(String, String)> = policy
386            .spec
387            .target_refs
388            .iter()
389            .filter_map(|t| service_gateways.get(&(namespace.clone(), t.name.clone())))
390            .flatten()
391            .cloned()
392            .collect();
393        let previous = policy.status.as_ref().map(|s| s.ancestors.as_slice()).unwrap_or_default();
394        let ancestors: Vec<PolicyAncestorStatus> = gateways
395            .into_iter()
396            .map(|(gateway_namespace, gateway_name)| {
397                let ancestor_ref = ParentReference {
398                    group: Some(GATEWAY_GROUP.to_string()),
399                    kind: Some("Gateway".to_string()),
400                    namespace: Some(gateway_namespace),
401                    name: gateway_name,
402                    section_name: None,
403                    port: None,
404                };
405                let old = previous.iter().find(|a| a.controller_name == controller_name && a.ancestor_ref == ancestor_ref).map(|a| a.conditions.as_slice()).unwrap_or_default();
406                let conditions = vec![
407                    match &evaluated.accepted {
408                        Ok(()) => condition(old, "Accepted", true, "Accepted", "", generation, now),
409                        Err((reason, message)) => condition(old, "Accepted", false, reason, message, generation, now),
410                    },
411                    match &evaluated.resolved {
412                        Ok(()) => condition(old, "ResolvedRefs", true, "ResolvedRefs", "", generation, now),
413                        Err((reason, message)) => condition(old, "ResolvedRefs", false, reason, message, generation, now),
414                    },
415                ];
416                PolicyAncestorStatus { ancestor_ref, controller_name: controller_name.to_string(), conditions }
417            })
418            .collect();
419        if !ancestors.is_empty() {
420            desired.backend_tls_policies.insert((namespace, name), ancestors);
421        }
422    }
423
424    // Gateway status now that attached routes are counted.
425    for gateway in &snapshot.gateways {
426        if !our_classes.contains(&gateway.spec.gateway_class_name) {
427            continue;
428        }
429        let key = (gateway.namespace().unwrap_or_default(), gateway.name_any());
430        let generation = gateway.metadata.generation;
431        let previous = gateway.status.clone().unwrap_or_default();
432        let container_ports = super::services::container_ports(super::services::current(snapshot, gateway));
433        let backend_client_cert = backend_client_certificate(gateway, snapshot);
434        let mut listener_statuses = Vec::new();
435        let mut set_statuses: BTreeMap<(String, String), Vec<ListenerStatus>> = BTreeMap::new();
436        let mut any_valid = false;
437        let (mut any_accepted, mut any_rejected) = (false, false);
438        for view in listeners.iter().filter(|l| l.gateway == key) {
439            let count = attached.get(&(key.clone(), view.owner.clone(), view.name.clone())).copied().unwrap_or(0);
440            match &view.owner {
441                None => {
442                    any_valid |= view.accepted && view.programmed;
443                    any_accepted |= view.accepted;
444                    any_rejected |= !view.accepted;
445                    let old = previous.listeners.iter().find(|l| l.name == view.name).map(|l| l.conditions.as_slice()).unwrap_or_default();
446                    listener_statuses.push(listener_status(view, count, old, generation, now));
447                }
448                Some(set_key) => {
449                    let set = snapshot.listener_sets.iter().find(|s| is_named(*s, set_key));
450                    let old = set.and_then(|s| s.status.as_ref()).and_then(|s| s.listeners.iter().find(|l| l.name == view.name)).map(|l| l.conditions.clone()).unwrap_or_default();
451                    let set_generation = set.and_then(|s| s.metadata.generation);
452                    set_statuses.entry(set_key.clone()).or_default().push(listener_status(view, count, &old, set_generation, now));
453                }
454            }
455            let container_port = container_ports.get(&(view.port, super::services::transport(&view.protocol).to_string()));
456            if let (true, true, Some(container_port)) = (view.accepted, view.programmed, container_port) {
457                config.listeners.push(ListenerConfig {
458                    gateway: key.clone(),
459                    name: view.name.clone(),
460                    port: view.port,
461                    protocol: view.protocol.clone(),
462                    hostname: view.hostname.clone(),
463                    container_port: *container_port,
464                    tls: view.tls.clone(),
465                    client_ca: view.client_ca.clone(),
466                    backend_client_cert: backend_client_cert.as_ref().and_then(|c| c.as_ref().ok()).cloned(),
467                    request_filters: view.request_filters.clone(),
468                    http_routes: view.http_routes.clone(),
469                });
470            }
471        }
472        let mut addresses = gateway_addresses(snapshot, &key.0, &key.1);
473        let unsupported_address = gateway.spec.addresses.iter().find(|a| !super::services::address_supported(a));
474        // Static addresses must all be the Service's.
475        let requested = super::services::requested_addresses(gateway);
476        let mut missing: Option<String> = requested.iter().find(|r| !addresses.iter().any(|a| &a.value == *r)).map(|r| r.to_string());
477        // Experimental (GEP-5093): every entry matched on its own, with routability.
478        let mut addresses_assigned = None;
479        if settings.experimental {
480            let service = super::services::current(snapshot, gateway);
481            let cluster_ips: Vec<String> = service
482                .and_then(|s| s.spec.as_ref())
483                .map(|s| s.cluster_ips.clone().unwrap_or_else(|| s.cluster_ip.clone().into_iter().collect()))
484                .unwrap_or_default()
485                .into_iter()
486                .filter(|ip| !ip.is_empty() && ip != "None")
487                .collect();
488            let assignment = super::addresses::assign(&gateway.spec.addresses, &addresses, &cluster_ips);
489            if service.is_some() {
490                addresses_assigned = Some(assignment.condition.clone());
491            }
492            if !assignment.condition.0 && missing.is_none() && !assignment.addresses.is_empty() {
493                missing = Some(assignment.condition.2.trim_start_matches("Not assigned: ").to_string());
494            }
495            addresses = assignment.addresses;
496        }
497        let addressed = !addresses.is_empty();
498        let programmed = any_valid && addressed && missing.is_none() && unsupported_address.is_none();
499        let (programmed_reason, programmed_message) = if let Some(address) = &missing {
500            ("AddressNotUsable", format!("The load balancer hasn't assigned {address}"))
501        } else if !addressed {
502            ("AddressNotAssigned", "Waiting for the Service's address".to_string())
503        } else if unsupported_address.is_some() {
504            ("Invalid", "An address type isn't supported".to_string())
505        } else if !any_valid {
506            ("Invalid", "No valid listener".to_string())
507        } else {
508            ("Programmed", "Serving".to_string())
509        };
510        // Accepted unless no listener is; `ListenersNotValid` when any isn't.
511        // rws takes no parameters object: a reference can't be honoured.
512        let invalid_parameters = gateway.spec.infrastructure.as_ref().is_some_and(|i| i.parameters_ref.is_some());
513        let gateway_accepted = (gateway.spec.listeners.is_empty() || any_accepted) && unsupported_address.is_none() && !invalid_parameters;
514        let accepted_reason = if invalid_parameters {
515            "InvalidParameters"
516        } else if unsupported_address.is_some() {
517            "UnsupportedAddress"
518        } else if any_rejected {
519            "ListenersNotValid"
520        } else {
521            "Accepted"
522        };
523
524        // Its ListenerSets: accepted with at least one valid listener.
525        let mut attached_sets = 0;
526        for (set, _, allowed) in listener_sets.iter().filter(|(_, gateway, _)| *gateway == key) {
527            let set_key = (set.namespace().unwrap_or_default(), set.name_any());
528            let statuses = set_statuses.remove(&set_key).unwrap_or_default();
529            let valid = statuses.iter().any(|l| l.conditions.iter().all(|c| c.type_ == "Conflicted" || c.type_ == "ResolvedRefs" || c.status == "True"));
530            let (ok, reason, message) = if !allowed {
531                (false, "NotAllowed", "The Gateway's allowedListeners doesn't admit this ListenerSet")
532            } else if !valid {
533                (false, "ListenersNotValid", "No valid listener")
534            } else {
535                (true, "Accepted", "")
536            };
537            attached_sets += ok as i32;
538            let old = set.status.as_ref().map(|s| s.conditions.as_slice()).unwrap_or_default();
539            let set_generation = set.metadata.generation;
540            let conditions = vec![
541                condition(old, "Accepted", ok, reason, message, set_generation, now),
542                condition(old, "Programmed", ok, if ok { "Programmed" } else { reason }, message, set_generation, now),
543            ];
544            desired.listener_sets.insert(set_key, ListenerSetStatus { conditions, listeners: statuses });
545        }
546        let mut conditions = vec![
547            condition(&previous.conditions, "Accepted", gateway_accepted, accepted_reason, "", generation, now),
548            condition(&previous.conditions, "Programmed", programmed, programmed_reason, &programmed_message, generation, now),
549        ];
550        if let Some(certificate) = &backend_client_cert {
551            conditions.push(match certificate {
552                Ok(_) => condition(&previous.conditions, "ResolvedRefs", true, "ResolvedRefs", "", generation, now),
553                Err((reason, message)) => condition(&previous.conditions, "ResolvedRefs", false, reason, message, generation, now),
554            });
555        }
556        if let Some((assigned, reason, message)) = &addresses_assigned {
557            conditions.push(condition(&previous.conditions, "AddressesAssigned", *assigned, reason, message, generation, now));
558        }
559        if insecure_frontend(gateway) {
560            let message = "Client certificate validation allows clients without a valid certificate (AllowInsecureFallback)";
561            conditions.push(condition(&previous.conditions, "InsecureFrontendValidationMode", true, "ConfigurationChanged", message, generation, now));
562        }
563        let conditions = with_foreign(conditions, &previous.conditions);
564        desired.gateways.insert(
565            key,
566            GatewayStatus { addresses, conditions, listeners: listener_statuses, attached_listener_sets: Some(attached_sets) },
567        );
568    }
569    (config, desired)
570}
571
572// ── listeners ─────────────────────────────────────────────────────────────────
573
574/// (Gateway, ListenerSet or `None` for the Gateway's own, listener name).
575type ListenerKey = ((String, String), Option<(String, String)>, String);
576
577struct ListenerView {
578    gateway: (String, String),
579    /// The ListenerSet it comes from; `None` for the Gateway's own.
580    owner: Option<(String, String)>,
581    /// Its object's namespace (`from: Same` for routes, Secrets).
582    namespace: String,
583    name: String,
584    port: i32,
585    protocol: String,
586    hostname: Option<String>,
587    allowed: AllowedRoutes,
588    supported_kinds: Vec<RouteGroupKind>,
589    accepted: bool,
590    accepted_reason: &'static str,
591    programmed: bool,
592    resolved_refs: Result<(), (&'static str, String)>,
593    conflicted: Option<&'static str>,
594    message: String,
595    tls: Option<(Vec<u8>, Vec<u8>)>,
596    client_ca: Option<ClientCa>,
597    request_filters: Vec<super::ext_auth::CompiledAuth>,
598    http_routes: Vec<CompiledHttpRoute>,
599}
600
601/// HTTPS listeners, and TLS listeners in `Terminate` mode (the default),
602/// decrypt with their certificates; `Passthrough` forwards the TLS stream.
603fn terminates_tls(listener: &Listener) -> bool {
604    match listener.protocol.as_str() {
605        "HTTPS" => true,
606        "TLS" => listener.tls.as_ref().and_then(|t| t.mode.as_deref()).unwrap_or("Terminate") == "Terminate",
607        _ => false,
608    }
609}
610
611/// The route kinds a protocol carries.
612fn kinds_for(protocol: &str) -> &'static [&'static str] {
613    match protocol {
614        "HTTP" | "HTTPS" => &["HTTPRoute", "GRPCRoute"],
615        "TLS" => &["TLSRoute"],
616        "TCP" => &["TCPRoute"],
617        "UDP" => &["UDPRoute"],
618        _ => &[],
619    }
620}
621
622/// The views of `listeners`, of the Gateway `key` (from the ListenerSet
623/// `owner`, if any, in `namespace`); conflicts are marked later, over the
624/// Gateway's merged list ([`mark_conflicts`]).
625fn listener_views(gateway: &Gateway, listeners: &[Listener], namespace: &str, owner: Option<(String, String)>, snapshot: &Snapshot) -> Vec<ListenerView> {
626    let key = (gateway.namespace().unwrap_or_default(), gateway.name_any());
627    listeners
628        .iter()
629        .map(|l| {
630            let allowed = l.allowed_routes.clone().unwrap_or_default();
631            let carried = kinds_for(&l.protocol);
632            let mut supported_kinds = Vec::new();
633            let mut resolved_refs = Ok(());
634            if allowed.kinds.is_empty() {
635                supported_kinds = carried.iter().map(|k| RouteGroupKind { group: Some(GATEWAY_GROUP.to_string()), kind: k.to_string() }).collect();
636            } else {
637                for kind in &allowed.kinds {
638                    let group_ok = kind.group.as_deref().unwrap_or(GATEWAY_GROUP) == GATEWAY_GROUP;
639                    if group_ok && carried.contains(&kind.kind.as_str()) {
640                        supported_kinds.push(RouteGroupKind { group: Some(GATEWAY_GROUP.to_string()), kind: kind.kind.clone() });
641                    } else {
642                        resolved_refs = Err(("InvalidRouteKinds", format!("{} isn't served on a {} listener", kind.kind, l.protocol)));
643                    }
644                }
645            }
646            let (mut accepted, mut accepted_reason, message) = if carried.is_empty() {
647                (false, "UnsupportedProtocol", format!("protocol {} isn't supported", l.protocol))
648            } else {
649                (true, "Accepted", String::new())
650            };
651            let (mut tls, mut client_ca) = (None, None);
652            if accepted && terminates_tls(l) {
653                match certificates(l, namespace, snapshot) {
654                    Ok(material) => tls = Some(material),
655                    Err(e) => resolved_refs = Err(e),
656                }
657                if let Some(validation) = frontend_validation(gateway, l.port) {
658                    match client_cas(validation, &key.0, snapshot) {
659                        Ok((ca, skipped)) => {
660                            client_ca = Some(ca);
661                            // An unusable ClusterTrustBundle beside valid sources.
662                            if let Some(e) = skipped {
663                                resolved_refs = Err(e);
664                            }
665                        }
666                        Err(e) => {
667                            resolved_refs = Err(e);
668                            accepted = false;
669                            accepted_reason = "NoValidCACertificate";
670                        }
671                    }
672                }
673            }
674            // Invalid route kinds alone don't stop the listener serving the valid ones.
675            // Pre-routing filters (GEP-5224): ExternalAuth on HTTP and
676            // HTTPS listeners; anything else can't be run in order, so the
677            // listener isn't accepted.
678            let mut request_filters = Vec::new();
679            let mut message = message;
680            for filter in l.filters.as_ref().map(|f| f.requests.as_slice()).unwrap_or_default() {
681                let auth = filter.external_auth.as_ref().filter(|_| filter.filter_type == "ExternalAuth" && matches!(l.protocol.as_str(), "HTTP" | "HTTPS"));
682                let Some(auth) = auth else {
683                    accepted = false;
684                    accepted_reason = "UnsupportedValue";
685                    message = format!("listener filter {} isn't supported on a {} listener", filter.filter_type, l.protocol);
686                    break;
687                };
688                let resolved = super::routes::resolve(&auth.backend_ref, namespace, "Gateway", snapshot);
689                request_filters.push(super::ext_auth::CompiledAuth { filter: auth.clone(), endpoints: resolved.endpoints, tls: resolved.tls });
690            }
691            // (Nor does a ClusterTrustBundle left out beside valid CA sources.)
692            let programmed = accepted && resolved_refs.as_ref().err().is_none_or(|(reason, _)| *reason == "InvalidRouteKinds" || (client_ca.is_some() && matches!(*reason, "InvalidCACertificateRef" | "InvalidKind")));
693            ListenerView {
694                gateway: key.clone(),
695                owner: owner.clone(),
696                namespace: namespace.to_string(),
697                name: l.name.clone(),
698                port: l.port,
699                protocol: l.protocol.clone(),
700                hostname: l.hostname.clone(),
701                allowed,
702                supported_kinds,
703                accepted,
704                accepted_reason,
705                programmed,
706                resolved_refs,
707                conflicted: None,
708                message,
709                tls,
710                client_ca,
711                request_filters,
712                http_routes: Vec::new(),
713            }
714        })
715        .collect()
716}
717
718/// Listeners on one port must agree on the protocol and differ in hostname.
719/// Among the Gateway's own listeners every party to a conflict is marked; a
720/// ListenerSet's listener that conflicts with one earlier in the merged list
721/// (the Gateway's, then older ListenerSets') is the one that loses, and
722/// isn't accepted.
723fn mark_conflicts(views: &mut [ListenerView]) {
724    for j in 0..views.len() {
725        for i in 0..views.len() {
726            if i == j || views[i].port != views[j].port {
727                continue;
728            }
729            // TCP and UDP listeners on one port number don't share a socket.
730            if super::services::transport(&views[i].protocol) != super::services::transport(&views[j].protocol) {
731                continue;
732            }
733            // A ListenerSet's listener only loses to earlier ones.
734            if views[j].owner.is_some() && (i > j || views[i].conflicted.is_some()) {
735                continue;
736            }
737            if views[j].owner.is_none() && views[i].owner.is_some() {
738                continue;
739            }
740            // Terminate and Passthrough listeners share a port
741            // (TLSRouteModeMixed): the SNI picks the listener.
742            let reason = if views[i].protocol != views[j].protocol {
743                "ProtocolConflict"
744            } else if views[i].hostname == views[j].hostname {
745                "HostnameConflict"
746            } else {
747                continue;
748            };
749            views[j].conflicted = Some(reason);
750            views[j].programmed = false;
751            if views[j].owner.is_some() {
752                views[j].accepted = false;
753                views[j].accepted_reason = reason;
754            }
755        }
756    }
757}
758
759/// The ListenerSets naming `gateway` as their parent, oldest first, and
760/// whether its `allowedListeners` admits each.
761fn attached_listener_sets<'a>(gateway: &Gateway, snapshot: &'a Snapshot) -> Vec<(&'a ListenerSet, bool)> {
762    let gateway_namespace = gateway.namespace().unwrap_or_default();
763    let mut sets: Vec<&ListenerSet> = snapshot
764        .listener_sets
765        .iter()
766        .filter(|set| {
767            let parent = &set.spec.parent_ref;
768            let namespace = parent.namespace.clone().or_else(|| set.namespace()).unwrap_or_default();
769            parent.group.as_deref().unwrap_or(GATEWAY_GROUP) == GATEWAY_GROUP
770                && parent.kind.as_deref().unwrap_or("Gateway") == "Gateway"
771                && parent.name == gateway.name_any()
772                && namespace == gateway_namespace
773        })
774        .collect();
775    sets.sort_by_key(|set| (set.metadata.creation_timestamp.as_ref().map(|t| t.0.to_string()).unwrap_or_default(), set.namespace(), set.name_any()));
776    let admitted = gateway.spec.allowed_listeners.as_ref().and_then(|a| a.namespaces.as_ref());
777    sets.into_iter()
778        .map(|set| {
779            let namespace = set.namespace().unwrap_or_default();
780            let allowed = namespace_admitted(admitted.and_then(|n| n.from.as_deref()).unwrap_or("None"), admitted.and_then(|n| n.selector.as_ref()), &namespace, &gateway_namespace, snapshot);
781            (set, allowed)
782        })
783        .collect()
784}
785
786/// `from` (`All`, `Same`, `Selector`, `None`) admits objects in `namespace`
787/// for an object in `home`.
788fn namespace_admitted(from: &str, selector: Option<&LabelSelector>, namespace: &str, home: &str, snapshot: &Snapshot) -> bool {
789    match from {
790        "All" => true,
791        "Same" => namespace == home,
792        "Selector" => {
793            let mut labels = snapshot.namespaces.iter().find(|n| n.name_any() == namespace).map(|n| n.labels().clone()).unwrap_or_default();
794            // The API server sets this on every namespace; selectors use it.
795            labels.entry("kubernetes.io/metadata.name".to_string()).or_insert_with(|| namespace.to_string());
796            selector.is_some_and(|s| selector_matches(s, &labels))
797        }
798        _ => false,
799    }
800}
801
802/// A listener's status from its view.
803/// `ours`, then the conditions of other controllers from `previous`, kept
804/// as they were: those with a domain-qualified type (`example.com/Ready`),
805/// which rws never writes.
806fn with_foreign(mut ours: Vec<Condition>, previous: &[Condition]) -> Vec<Condition> {
807    ours.extend(previous.iter().filter(|c| c.type_.contains('/') && !ours.iter().any(|o| o.type_ == c.type_)).cloned().collect::<Vec<_>>());
808    ours
809}
810
811fn listener_status(view: &ListenerView, attached_routes: i32, old: &[Condition], generation: Option<i64>, now: &Time) -> ListenerStatus {
812    let programmed_reason = match (view.programmed, view.conflicted) {
813        (true, _) => "Programmed",
814        (false, Some(reason)) if view.owner.is_some() => reason,
815        _ => "Invalid",
816    };
817    ListenerStatus {
818        name: view.name.clone(),
819        supported_kinds: view.supported_kinds.clone(),
820        attached_routes,
821        conditions: with_foreign(
822            vec![
823                condition(old, "Accepted", view.accepted, view.accepted_reason, &view.message, generation, now),
824                condition(old, "Programmed", view.programmed, programmed_reason, &view.message, generation, now),
825                condition(old, "ResolvedRefs", view.resolved_refs.is_ok(), view.resolved_refs.as_ref().err().map_or("ResolvedRefs", |(r, _)| r), view.resolved_refs.as_ref().err().map_or("", |(_, m)| m.as_str()), generation, now),
826                condition(old, "Conflicted", view.conflicted.is_some(), view.conflicted.unwrap_or("NoConflicts"), "", generation, now),
827            ],
828            old,
829        ),
830    }
831}
832
833/// An HTTPS listener's certificates resolve: TLS Secrets in its namespace,
834/// or in another one a ReferenceGrant opens.
835/// A listener's PEM certificate chain and key.
836type TlsMaterial = (Vec<u8>, Vec<u8>);
837
838fn certificates(listener: &Listener, gateway_namespace: &str, snapshot: &Snapshot) -> Result<TlsMaterial, (&'static str, String)> {
839    let Some(tls) = &listener.tls else {
840        return Err(("InvalidCertificateRef", "a listener that terminates TLS needs tls.certificateRefs".to_string()));
841    };
842    if tls.certificate_refs.is_empty() {
843        return Err(("InvalidCertificateRef", "a listener that terminates TLS needs tls.certificateRefs".to_string()));
844    }
845    let mut material = None;
846    for cert in &tls.certificate_refs {
847        let group = cert.group.as_deref().unwrap_or("");
848        let kind = cert.kind.as_deref().unwrap_or("Secret");
849        if !group.is_empty() || kind != "Secret" {
850            return Err(("InvalidCertificateRef", format!("{kind} isn't a supported certificate reference")));
851        }
852        let namespace = cert.namespace.clone().unwrap_or_else(|| gateway_namespace.to_string());
853        if namespace != gateway_namespace && !granted(snapshot, GATEWAY_GROUP, "Gateway", gateway_namespace, "", "Secret", &namespace, &cert.name) {
854            return Err(("RefNotPermitted", format!("no ReferenceGrant in {namespace} lets Gateways in {gateway_namespace} use Secret {}", cert.name)));
855        }
856        let secret = snapshot.secrets.iter().find(|s| s.namespace().as_deref() == Some(namespace.as_str()) && s.name_any() == cert.name);
857        let valid = secret.is_some_and(|s| {
858            let data = s.data.as_ref();
859            let pem = |key: &str| data.and_then(|d| d.get(key)).is_some_and(|v| !v.0.is_empty());
860            pem("tls.crt") && pem("tls.key") && crate::core::x509::cert_info_from_pem(&data.unwrap()["tls.crt"].0).is_some()
861        });
862        if !valid {
863            return Err(("InvalidCertificateRef", format!("Secret {namespace}/{} isn't a valid TLS Secret", cert.name)));
864        }
865        if material.is_none() {
866            let data = secret.and_then(|s| s.data.as_ref()).expect("checked above");
867            material = Some((data["tls.crt"].0.clone(), data["tls.key"].0.clone()));
868        }
869    }
870    material.ok_or(("InvalidCertificateRef", "no certificate".to_string()))
871}
872
873/// The client-certificate validation for a listener port: the Gateway's
874/// `spec.tls.frontend.perPort` entry for it, else the default.
875fn frontend_validation(gateway: &Gateway, port: i32) -> Option<&FrontendTlsValidation> {
876    let frontend = gateway.spec.tls.as_ref()?.frontend.as_ref()?;
877    match frontend.per_port.iter().find(|p| p.port == port) {
878        Some(per_port) => per_port.tls.validation.as_ref(),
879        None => frontend.default.validation.as_ref(),
880    }
881}
882
883/// The Gateway's backend client certificate, when it names one: a TLS
884/// Secret in its namespace, or one a ReferenceGrant opens.
885fn backend_client_certificate(gateway: &Gateway, snapshot: &Snapshot) -> Option<Result<TlsMaterial, (&'static str, String)>> {
886    let reference = gateway.spec.tls.as_ref()?.backend.as_ref()?.client_certificate_ref.as_ref()?;
887    let gateway_namespace = gateway.namespace().unwrap_or_default();
888    let group = reference.group.as_deref().unwrap_or("");
889    let kind = reference.kind.as_deref().unwrap_or("Secret");
890    if !group.is_empty() || kind != "Secret" {
891        return Some(Err(("InvalidClientCertificateRef", format!("{group}/{kind} isn't a supported client certificate reference"))));
892    }
893    let namespace = reference.namespace.clone().unwrap_or_else(|| gateway_namespace.clone());
894    if namespace != gateway_namespace && !granted(snapshot, GATEWAY_GROUP, "Gateway", &gateway_namespace, "", "Secret", &namespace, &reference.name) {
895        return Some(Err(("RefNotPermitted", format!("no ReferenceGrant in {namespace} lets Gateways in {gateway_namespace} use Secret {}", reference.name))));
896    }
897    let data = snapshot
898        .secrets
899        .iter()
900        .find(|s| s.namespace().as_deref() == Some(namespace.as_str()) && s.name_any() == reference.name)
901        .and_then(|s| s.data.as_ref());
902    let pair = data.and_then(|d| Some((d.get("tls.crt")?.0.clone(), d.get("tls.key")?.0.clone())));
903    let valid = pair.as_ref().filter(|(cert, key)| crate::tls::certified_key_from_pem(cert, key).is_ok());
904    Some(valid.cloned().ok_or(("InvalidClientCertificateRef", format!("Secret {namespace}/{} isn't a valid TLS Secret", reference.name))))
905}
906
907/// Whether any of the Gateway's validation settings is `AllowInsecureFallback`.
908fn insecure_frontend(gateway: &Gateway) -> bool {
909    let Some(frontend) = gateway.spec.tls.as_ref().and_then(|t| t.frontend.as_ref()) else { return false };
910    std::iter::once(&frontend.default)
911        .chain(frontend.per_port.iter().map(|p| &p.tls))
912        .filter_map(|t| t.validation.as_ref())
913        .any(|v| v.mode.as_deref() == Some("AllowInsecureFallback"))
914}
915
916/// The group and kind of a ClusterTrustBundle reference (GEP-5075).
917pub const CLUSTER_TRUST_BUNDLE_GROUP: &str = "certificates.k8s.io";
918
919/// A ClusterTrustBundle's PEM certificates: `InvalidKind` when the cluster
920/// doesn't serve the API, `InvalidCACertificateRef` for a missing, empty or
921/// unparsable bundle (GEP-5075).
922pub fn cluster_trust_bundle<'a>(snapshot: &'a Snapshot, name: &str) -> Result<&'a str, (&'static str, String)> {
923    if !snapshot.cluster_trust_bundles_served {
924        return Err(("InvalidKind", "this cluster doesn't serve ClusterTrustBundles (certificates.k8s.io)".to_string()));
925    }
926    snapshot
927        .cluster_trust_bundles
928        .iter()
929        .find(|(n, _)| n == name)
930        .map(|(_, pem)| pem.as_str())
931        .filter(|pem| crate::core::x509::cert_info_from_pem(pem.as_bytes()).is_some())
932        .ok_or_else(|| ("InvalidCACertificateRef", format!("ClusterTrustBundle {name} doesn't exist or holds no valid certificate")))
933}
934
935/// The CA certificates a validation references: ConfigMaps with `ca.crt`,
936/// in the Gateway's namespace or one a ReferenceGrant opens, and
937/// ClusterTrustBundles. An unusable ConfigMap fails the validation; an
938/// unusable bundle is left out and reported (the second value) while other
939/// sources remain (GEP-5075).
940fn client_cas(validation: &FrontendTlsValidation, gateway_namespace: &str, snapshot: &Snapshot) -> Result<(ClientCa, Option<(&'static str, String)>), (&'static str, String)> {
941    let mut pem = Vec::new();
942    let mut skipped = None;
943    for reference in &validation.ca_certificate_refs {
944        if reference.group == CLUSTER_TRUST_BUNDLE_GROUP && reference.kind == "ClusterTrustBundle" {
945            match cluster_trust_bundle(snapshot, &reference.name) {
946                Ok(bundle) => {
947                    pem.extend_from_slice(bundle.as_bytes());
948                    pem.push(b'\n');
949                }
950                Err(e) => skipped = Some(e),
951            }
952            continue;
953        }
954        if !reference.group.is_empty() || reference.kind != "ConfigMap" {
955            return Err(("InvalidCACertificateKind", format!("{} isn't a supported CA certificate reference", reference.kind)));
956        }
957        let namespace = reference.namespace.clone().unwrap_or_else(|| gateway_namespace.to_string());
958        if namespace != gateway_namespace && !granted(snapshot, GATEWAY_GROUP, "Gateway", gateway_namespace, "", "ConfigMap", &namespace, &reference.name) {
959            return Err(("RefNotPermitted", format!("no ReferenceGrant in {namespace} lets Gateways in {gateway_namespace} use ConfigMap {}", reference.name)));
960        }
961        let ca = snapshot
962            .config_maps
963            .iter()
964            .find(|c| c.namespace().as_deref() == Some(namespace.as_str()) && c.name_any() == reference.name)
965            .and_then(|c| c.data.as_ref())
966            .and_then(|d| d.get("ca.crt"))
967            .filter(|ca| crate::core::x509::cert_info_from_pem(ca.as_bytes()).is_some());
968        let Some(ca) = ca else {
969            return Err(("InvalidCACertificateRef", format!("ConfigMap {namespace}/{} has no valid ca.crt", reference.name)));
970        };
971        pem.extend_from_slice(ca.as_bytes());
972        pem.push(b'\n');
973    }
974    if pem.is_empty() {
975        return Err(skipped.unwrap_or(("InvalidCACertificateRef", "no caCertificateRefs".to_string())));
976    }
977    Ok((ClientCa { pem, insecure_fallback: validation.mode.as_deref() == Some("AllowInsecureFallback") }, skipped))
978}
979
980// ── routes ────────────────────────────────────────────────────────────────────
981
982/// Whether a parentRef in a route's status names the same parent.
983fn same_parent(a: &ParentReference, b: &ParentReference, route_namespace: &str) -> bool {
984    a.group() == b.group()
985        && a.kind() == b.kind()
986        && a.name == b.name
987        && a.namespace.as_deref().unwrap_or(route_namespace) == b.namespace.as_deref().unwrap_or(route_namespace)
988        && a.section_name == b.section_name
989        && a.port == b.port
990}
991
992fn is_named<K: kube::Resource>(object: &K, key: &(String, String)) -> bool {
993    object.meta().namespace.as_deref() == Some(key.0.as_str()) && object.meta().name.as_deref() == Some(key.1.as_str())
994}
995
996/// Attach `route` through `parent_ref` to the matching listeners: (accepted,
997/// reason, message, listener names).
998fn attach(kind: &str, hostnames: &[String], parent_ref: &ParentReference, route_namespace: &str, candidates: &[&ListenerView], snapshot: &Snapshot) -> (bool, &'static str, String, Vec<String>) {
999    let mut named: Vec<&&ListenerView> = candidates
1000        .iter()
1001        .filter(|l| parent_ref.section_name.as_ref().is_none_or(|s| *s == l.name))
1002        .filter(|l| parent_ref.port.is_none_or(|p| p == l.port))
1003        .collect();
1004    if named.is_empty() {
1005        return (false, "NoMatchingParent", "no listener matches the parentRef's sectionName/port".to_string(), Vec::new());
1006    }
1007    named.retain(|l| l.accepted && l.conflicted.is_none());
1008    let allowed: Vec<&&ListenerView> = named.iter().copied().filter(|l| allows(l, kind, route_namespace, snapshot)).collect();
1009    if allowed.is_empty() {
1010        return (false, "NotAllowedByListeners", "no listener allows this route".to_string(), Vec::new());
1011    }
1012    let matching: Vec<String> = allowed
1013        .iter()
1014        .filter(|l| hostnames_intersect(l.hostname.as_deref(), hostnames))
1015        .map(|l| l.name.clone())
1016        .collect();
1017    if matching.is_empty() {
1018        return (false, "NoMatchingListenerHostname", "no listener hostname matches the route's hostnames".to_string(), Vec::new());
1019    }
1020    (true, "Accepted", String::new(), matching)
1021}
1022
1023/// A listener allows routes of `kind` from `namespace`.
1024fn allows(listener: &ListenerView, kind: &str, namespace: &str, snapshot: &Snapshot) -> bool {
1025    if !listener.supported_kinds.iter().any(|k| k.kind == kind) {
1026        return false;
1027    }
1028    let namespaces = listener.allowed.namespaces.as_ref();
1029    let from = namespaces.and_then(|n| n.from.as_deref()).unwrap_or("Same");
1030    namespace_admitted(from, namespaces.and_then(|n| n.selector.as_ref()), namespace, &listener.namespace, snapshot)
1031}
1032
1033#[doc(hidden)]
1034pub fn selector_matches(selector: &LabelSelector, labels: &BTreeMap<String, String>) -> bool {
1035    let labels_ok = selector.match_labels.as_ref().is_none_or(|m| m.iter().all(|(k, v)| labels.get(k) == Some(v)));
1036    let expressions_ok = selector.match_expressions.as_ref().is_none_or(|exprs| {
1037        exprs.iter().all(|e| {
1038            let value = labels.get(&e.key);
1039            let values = e.values.clone().unwrap_or_default();
1040            match e.operator.as_str() {
1041                "In" => value.is_some_and(|v| values.contains(v)),
1042                "NotIn" => value.is_none_or(|v| !values.contains(v)),
1043                "Exists" => value.is_some(),
1044                "DoesNotExist" => value.is_none(),
1045                _ => false,
1046            }
1047        })
1048    });
1049    labels_ok && expressions_ok
1050}
1051
1052/// A listener's hostname and a route's hostnames have something in common
1053/// (either side empty = any; `*.example.com` covers one or more labels).
1054#[doc(hidden)]
1055pub fn hostnames_intersect(listener: Option<&str>, route: &[String]) -> bool {
1056    let Some(listener) = listener.filter(|h| !h.is_empty()) else { return true };
1057    route.is_empty() || route.iter().any(|r| hostname_overlap(listener, r))
1058}
1059
1060fn hostname_overlap(a: &str, b: &str) -> bool {
1061    let a = a.to_ascii_lowercase();
1062    let b = b.to_ascii_lowercase();
1063    match (a.strip_prefix("*."), b.strip_prefix("*.")) {
1064        (None, None) => a == b,
1065        (Some(suffix), None) => b.ends_with(&format!(".{suffix}")),
1066        (None, Some(suffix)) => a.ends_with(&format!(".{suffix}")),
1067        (Some(x), Some(y)) => x == y || x.ends_with(&format!(".{y}")) || y.ends_with(&format!(".{x}")),
1068    }
1069}
1070
1071/// A route of any kind, as the attachment loop needs it.
1072struct RouteIn<'a> {
1073    kind: &'static str,
1074    namespace: String,
1075    name: String,
1076    generation: Option<i64>,
1077    parent_refs: &'a [ParentReference],
1078    hostnames: &'a [String],
1079    previous: Vec<RouteParentStatus>,
1080    /// Every Service the route sends to (backendRefs and mirrors).
1081    references: Vec<&'a BackendObjectReference>,
1082    compiled: CompiledHttpRoute,
1083}
1084
1085impl<'a> RouteIn<'a> {
1086    fn new<S>(
1087        kind: &'static str,
1088        route: &'a kube::core::Object<S, RouteStatus>,
1089        parent_refs: &'a [ParentReference],
1090        hostnames: &'a [String],
1091        references: Vec<&'a BackendObjectReference>,
1092        compiled: CompiledHttpRoute,
1093    ) -> RouteIn<'a>
1094    where
1095        S: Clone,
1096    {
1097        RouteIn {
1098            kind,
1099            namespace: route.namespace().unwrap_or_default(),
1100            name: route.name_any(),
1101            generation: route.metadata.generation,
1102            parent_refs,
1103            hostnames,
1104            previous: route.status.as_ref().map(|s| s.parents.clone()).unwrap_or_default(),
1105            references,
1106            compiled,
1107        }
1108    }
1109}
1110
1111/// The Services a rule's backendRefs and `RequestMirror` filters (on the
1112/// rule or a backendRef) name.
1113fn rule_references<'a>(backend_refs: &'a [HttpBackendRef], filters: &'a [HttpRouteFilter]) -> impl Iterator<Item = &'a BackendObjectReference> {
1114    let mirrors = filters.iter().chain(backend_refs.iter().flat_map(|b| &b.filters)).filter_map(|f| f.request_mirror.as_ref());
1115    backend_refs.iter().map(|b| &b.backend_ref.object).chain(mirrors.map(|m| &m.backend_ref))
1116}
1117
1118fn http_references(rules: &[HttpRouteRule]) -> Vec<&BackendObjectReference> {
1119    rules.iter().flat_map(|r| rule_references(&r.backend_refs, &r.filters)).collect()
1120}
1121
1122fn l4_references(rules: &[L4RouteRule]) -> Vec<&BackendObjectReference> {
1123    rules.iter().flat_map(|r| r.backend_refs.iter().map(|b| &b.object)).collect()
1124}
1125
1126fn grpc_references(rules: &[GrpcRouteRule]) -> Vec<&BackendObjectReference> {
1127    rules.iter().flat_map(|r| rule_references(&r.backend_refs, &r.filters)).collect()
1128}
1129
1130/// Every reference resolves: a Service, in the route's namespace or one a
1131/// ReferenceGrant opens to routes of `kind`, that exists.
1132fn resolve_references(kind: &str, namespace: &str, references: &[&BackendObjectReference], snapshot: &Snapshot) -> Result<(), (&'static str, String)> {
1133    for reference in references {
1134        if !reference.group().is_empty() || reference.kind() != "Service" {
1135            return Err(("InvalidKind", format!("{} isn't a supported backend kind", reference.kind())));
1136        }
1137        let target = reference.namespace.clone().unwrap_or_else(|| namespace.to_string());
1138        if target != namespace && !granted(snapshot, GATEWAY_GROUP, kind, namespace, "", "Service", &target, &reference.name) {
1139            return Err(("RefNotPermitted", format!("no ReferenceGrant in {target} lets {kind}s in {namespace} use Service {}", reference.name)));
1140        }
1141        if !snapshot.services.iter().any(|s| s.namespace().as_deref() == Some(target.as_str()) && s.name_any() == reference.name) {
1142            return Err(("BackendNotFound", format!("Service {target}/{} doesn't exist", reference.name)));
1143        }
1144    }
1145    Ok(())
1146}
1147
1148/// A ReferenceGrant in `to_namespace` lets `from_kind`s in `from_namespace`
1149/// refer to the `to_kind` named `to_name`.
1150#[allow(clippy::too_many_arguments)]
1151#[doc(hidden)]
1152pub fn granted(snapshot: &Snapshot, from_group: &str, from_kind: &str, from_namespace: &str, to_group: &str, to_kind: &str, to_namespace: &str, to_name: &str) -> bool {
1153    snapshot.reference_grants.iter().filter(|g| g.namespace().as_deref() == Some(to_namespace)).any(|g| {
1154        g.spec.from.iter().any(|f| f.group == from_group && f.kind == from_kind && f.namespace == from_namespace)
1155            && g.spec.to.iter().any(|t| t.group == to_group && t.kind == to_kind && t.name.as_deref().is_none_or(|n| n == to_name))
1156    })
1157}
1158
1159// ── conditions ────────────────────────────────────────────────────────────────
1160
1161/// A condition, keeping `lastTransitionTime` from `previous` when the status
1162/// didn't change.
1163fn condition(previous: &[Condition], kind: &str, status: bool, reason: &str, message: &str, generation: Option<i64>, now: &Time) -> Condition {
1164    let status = if status { "True" } else { "False" }.to_string();
1165    let since = previous
1166        .iter()
1167        .find(|c| c.type_ == kind && c.status == status)
1168        .map(|c| c.last_transition_time.clone())
1169        .unwrap_or_else(|| now.clone());
1170    Condition {
1171        type_: kind.to_string(),
1172        status,
1173        reason: reason.to_string(),
1174        message: message.to_string(),
1175        observed_generation: generation,
1176        last_transition_time: since,
1177    }
1178}
1179
1180#[cfg(test)]
1181mod tests;