Skip to main content

rws_gateway_api/gateway_api/
addresses.rs

1//! A Gateway's status addresses and their routability (Gateway API v1.7,
2//! experimental, GEP-5093), from its Service: the load balancer's ingress
3//! addresses and the Service's ClusterIPs.
4//!
5//! Each `spec.addresses` entry is matched on its own, by a distinct
6//! address:
7//! - routability `""` (or unset): a load-balancer address — rws's default
8//!   scope, with no reachability promise beyond the load balancer's;
9//! - `Cluster`: one of the Service's ClusterIPs (a requested value is asked
10//!   of the Service as its `clusterIP`);
11//! - `testing.x-k8s.io/sentinel` (reserved for conformance, no guarantee):
12//!   like `""`, reported as asked;
13//! - anything else: rws can't satisfy it.
14//!
15//! An entry with a value matches only that address. With `spec.addresses`
16//! empty, every load-balancer address is reported, with routability `""`.
17
18use super::types::{GatewaySpecAddress, GatewayStatusAddress};
19
20/// The routability reserved for conformance tests.
21pub const SENTINEL: &str = "testing.x-k8s.io/sentinel";
22/// A ClusterIP.
23pub const CLUSTER: &str = "Cluster";
24
25/// The status addresses and the `AddressesAssigned` condition (status,
26/// reason, message).
27#[derive(Debug, PartialEq)]
28pub struct Assignment {
29    pub addresses: Vec<GatewayStatusAddress>,
30    pub condition: (bool, &'static str, String),
31}
32
33/// Whether rws can give an entry's routability at all.
34pub fn routability_supported(routability: &str) -> bool {
35    matches!(routability, "" | CLUSTER | SENTINEL)
36}
37
38/// The address a `Cluster` entry asks to be the Service's `clusterIP`.
39pub fn requested_cluster_ip(spec: &[GatewaySpecAddress]) -> Option<&str> {
40    spec.iter().find(|a| a.routability.as_deref() == Some(CLUSTER) && !a.value.is_empty() && is_ip(a)).map(|a| a.value.as_str())
41}
42
43fn is_ip(address: &GatewaySpecAddress) -> bool {
44    address.address_type.as_deref().unwrap_or("IPAddress") == "IPAddress"
45}
46
47/// Match `spec` against the load-balancer addresses and ClusterIPs.
48pub fn assign(spec: &[GatewaySpecAddress], load_balancer: &[GatewayStatusAddress], cluster_ips: &[String]) -> Assignment {
49    if spec.is_empty() {
50        let addresses = load_balancer.iter().map(|a| GatewayStatusAddress { routability: Some(String::new()), ..a.clone() }).collect();
51        return Assignment { addresses, condition: (true, "AddressesAssigned", "All addresses are assigned".to_string()) };
52    }
53    let mut used: Vec<String> = Vec::new();
54    let mut addresses = Vec::new();
55    let mut unsatisfied = Vec::new();
56    for entry in spec {
57        let routability = entry.routability.clone().unwrap_or_default();
58        let candidates: Vec<(Option<String>, String)> = match routability.as_str() {
59            _ if !is_ip(entry) => Vec::new(),
60            "" | SENTINEL => load_balancer.iter().map(|a| (a.address_type.clone(), a.value.clone())).collect(),
61            CLUSTER => cluster_ips.iter().map(|ip| (Some("IPAddress".to_string()), ip.clone())).collect(),
62            _ => Vec::new(),
63        };
64        let found = candidates.into_iter().find(|(_, value)| !used.contains(value) && (entry.value.is_empty() || *value == entry.value));
65        match found {
66            Some((address_type, value)) => {
67                used.push(value.clone());
68                addresses.push(GatewayStatusAddress { address_type, value, routability: Some(routability) });
69            }
70            None => unsatisfied.push(describe(entry)),
71        }
72    }
73    let condition = if unsatisfied.is_empty() {
74        (true, "AddressesAssigned", "All addresses are assigned".to_string())
75    } else if addresses.is_empty() {
76        (false, "NotAssigned", format!("Not assigned: {}", unsatisfied.join(", ")))
77    } else {
78        (false, "PartiallyAssigned", format!("Not assigned: {}", unsatisfied.join(", ")))
79    };
80    Assignment { addresses, condition }
81}
82
83fn describe(entry: &GatewaySpecAddress) -> String {
84    let value = if entry.value.is_empty() { "any address" } else { entry.value.as_str() };
85    match entry.routability.as_deref().unwrap_or("") {
86        "" => value.to_string(),
87        routability => format!("{value} ({routability})"),
88    }
89}
90
91#[cfg(test)]
92mod tests;