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