Skip to main content

rws_gateway_api/gateway_api/
mod.rs

1//! A Kubernetes Gateway API controller (`gateway-api` feature, 18.8.0, #19):
2//! rws watches GatewayClasses, Gateways and routes, serves their traffic, and
3//! reports what it accepted in their status. The plan and progress are in
4//! `spec/GATEWAY_API.md`.
5//!
6//! - [`types`] — the Gateway API v1.6 objects rws reads (serde structs).
7//! - [`resources`] — where each kind lives in the API, and the object types.
8//! - [`translate`] — the pure step from a snapshot of the cluster to the
9//!   configuration rws serves and the status every object should have.
10//! - [`services`] — the Service each Gateway gets, and its port allocation.
11//! - [`routes`] — routes compiled for the data plane, backends resolved to
12//!   endpoints.
13//! - [`dataplane`] / [`router`] — the ports served and how a request on one
14//!   finds its listener, rule, backend and endpoint.
15//! - [`controller`] — watches, the reconcile loop and status writes.
16
17pub mod backend_tls;
18pub mod controller;
19pub mod dataplane;
20pub mod filters;
21pub mod l4;
22pub mod lease;
23pub mod provision;
24pub mod resources;
25pub mod router;
26pub mod routes;
27pub mod services;
28pub mod translate;
29pub mod types;
30pub(crate) use crate::proxy::upgrade;
31
32/// The `controllerName` GatewayClasses name to be handled by rws.
33pub const CONTROLLER_NAME: &str = "rws8.tech/gateway-controller";
34
35/// The field manager rws writes status with.
36pub const FIELD_MANAGER: &str = "rws-gateway";
37
38use std::collections::BTreeMap;
39use std::sync::atomic::AtomicBool;
40use std::sync::Arc;
41
42/// `RWS_CONFIG_GATEWAY_API_ENABLED` (`[gateway_api] enabled`): run the
43/// controller and serve Gateways.
44pub const RWS_CONFIG_GATEWAY_API_ENABLED: &str = "RWS_CONFIG_GATEWAY_API_ENABLED";
45/// The `controllerName` handled (default [`CONTROLLER_NAME`]).
46pub const RWS_CONFIG_GATEWAY_API_CONTROLLER_NAME: &str = "RWS_CONFIG_GATEWAY_API_CONTROLLER_NAME";
47/// The labels of the pods Gateways' Services select, `key=value,…`.
48pub const RWS_CONFIG_GATEWAY_API_POD_SELECTOR: &str = "RWS_CONFIG_GATEWAY_API_POD_SELECTOR";
49/// Container ports to allocate, `first-last` (default `10000-10999`).
50pub const RWS_CONFIG_GATEWAY_API_PORT_RANGE: &str = "RWS_CONFIG_GATEWAY_API_PORT_RANGE";
51/// Gateways' Service type (default `LoadBalancer`).
52pub const RWS_CONFIG_GATEWAY_API_SERVICE_TYPE: &str = "RWS_CONFIG_GATEWAY_API_SERVICE_TYPE";
53/// Where the leader-election Lease lives (default: the pod's namespace).
54pub const RWS_CONFIG_GATEWAY_API_LEASE_NAMESPACE: &str = "RWS_CONFIG_GATEWAY_API_LEASE_NAMESPACE";
55/// `shared` (default: the controller's pods serve every Gateway) or
56/// `provisioned` (a Deployment per Gateway, [`provision`]).
57pub const RWS_CONFIG_GATEWAY_API_MODE: &str = "RWS_CONFIG_GATEWAY_API_MODE";
58/// The image provisioned Deployments run.
59pub const RWS_CONFIG_GATEWAY_API_IMAGE: &str = "RWS_CONFIG_GATEWAY_API_IMAGE";
60/// The ServiceAccount provisioned pods run as (default: the pod's, from
61/// `POD_SERVICE_ACCOUNT`).
62pub const RWS_CONFIG_GATEWAY_API_SERVICE_ACCOUNT: &str = "RWS_CONFIG_GATEWAY_API_SERVICE_ACCOUNT";
63/// Set on provisioned pods: `namespace/name` of the one Gateway they serve;
64/// they don't take the Lease or write anything.
65pub const RWS_CONFIG_GATEWAY_API_GATEWAY: &str = "RWS_CONFIG_GATEWAY_API_GATEWAY";
66
67fn setting(key: &str) -> Option<String> {
68    std::env::var(key).ok().map(|v| v.trim().to_string()).filter(|v| !v.is_empty())
69}
70
71/// Whether the controller is turned on.
72pub fn enabled() -> bool {
73    setting(RWS_CONFIG_GATEWAY_API_ENABLED).is_some_and(|v| matches!(v.to_ascii_lowercase().as_str(), "true" | "1" | "yes" | "on"))
74}
75
76/// `key=value,key=value`.
77pub fn parse_labels(value: &str) -> BTreeMap<String, String> {
78    value.split(',').filter_map(|pair| pair.split_once('=')).map(|(k, v)| (k.trim().to_string(), v.trim().to_string())).filter(|(k, _)| !k.is_empty()).collect()
79}
80
81/// `first-last`.
82pub fn parse_port_range(value: &str) -> Option<(u16, u16)> {
83    let (first, last) = value.split_once('-')?;
84    let (first, last) = (first.trim().parse::<u16>().ok()?, last.trim().parse::<u16>().ok()?);
85    (first >= 1024 && first <= last).then_some((first, last))
86}
87
88/// The translation settings, from `RWS_CONFIG_GATEWAY_API_*`.
89pub fn settings_from_env() -> translate::Settings {
90    let mut settings = translate::Settings::default();
91    if let Some(name) = setting(RWS_CONFIG_GATEWAY_API_CONTROLLER_NAME) {
92        settings.controller_name = name;
93    }
94    if let Some(selector) = setting(RWS_CONFIG_GATEWAY_API_POD_SELECTOR).map(|v| parse_labels(&v)).filter(|l| !l.is_empty()) {
95        settings.services.pod_selector = selector;
96    }
97    if let Some(range) = setting(RWS_CONFIG_GATEWAY_API_PORT_RANGE).and_then(|v| parse_port_range(&v)) {
98        settings.services.port_range = range;
99    }
100    if let Some(kind) = setting(RWS_CONFIG_GATEWAY_API_SERVICE_TYPE) {
101        settings.services.service_type = kind;
102    }
103    settings.services.pod_namespace = pod_namespace();
104    if setting(RWS_CONFIG_GATEWAY_API_MODE).is_some_and(|m| m.eq_ignore_ascii_case("provisioned")) {
105        settings.services.provisioned = true;
106        settings.provision = Some(provision::ProvisionSettings {
107            image: setting(RWS_CONFIG_GATEWAY_API_IMAGE).unwrap_or_else(|| format!("ghcr.io/bohdaq/rws:{}", env!("CARGO_PKG_VERSION"))),
108            service_account: setting(RWS_CONFIG_GATEWAY_API_SERVICE_ACCOUNT).or_else(|| setting("POD_SERVICE_ACCOUNT")).unwrap_or_else(|| "default".to_string()),
109            ..Default::default()
110        });
111    }
112    settings
113}
114
115/// `namespace/name` from [`RWS_CONFIG_GATEWAY_API_GATEWAY`]: this pod is a
116/// provisioned data plane for that Gateway only.
117pub fn own_gateway() -> Option<(String, String)> {
118    let value = setting(RWS_CONFIG_GATEWAY_API_GATEWAY)?;
119    let (namespace, name) = value.split_once('/')?;
120    (!namespace.is_empty() && !name.is_empty()).then(|| (namespace.to_string(), name.to_string()))
121}
122
123/// The pod's namespace, where the serving pods run: `POD_NAMESPACE`, or
124/// the service account's.
125fn pod_namespace() -> String {
126    setting("POD_NAMESPACE")
127        .or_else(|| std::fs::read_to_string("/var/run/secrets/kubernetes.io/serviceaccount/namespace").ok().map(|n| n.trim().to_string()))
128        .unwrap_or_else(|| "default".to_string())
129}
130
131/// Where the Lease lives: the setting, else the pod's namespace.
132fn lease_namespace() -> String {
133    setting(RWS_CONFIG_GATEWAY_API_LEASE_NAMESPACE).unwrap_or_else(pod_namespace)
134}
135
136/// When enabled, start the controller, leader election and data plane on
137/// the current tokio runtime, against the cluster rws runs in (or the
138/// kubeconfig). Returns at once.
139pub fn start_from_env() {
140    if !enabled() {
141        return;
142    }
143    tokio::spawn(async {
144        let client = match kube::Client::try_default().await {
145            Ok(client) => client,
146            Err(e) => {
147                eprintln!("[gateway-api] no Kubernetes API: {e}");
148                return;
149            }
150        };
151        let settings = settings_from_env();
152        let leader = Arc::new(AtomicBool::new(false));
153        let became_leader = Arc::new(tokio::sync::Notify::new());
154        let own = own_gateway();
155        match &own {
156            // A provisioned data plane: never the leader.
157            Some((namespace, name)) => println!("[gateway-api] serving Gateway {namespace}/{name}"),
158            None => {
159                println!("[gateway-api] serving GatewayClasses with controllerName {}", settings.controller_name);
160                let identity = setting("POD_NAME").or_else(|| setting("HOSTNAME")).unwrap_or_else(|| format!("rws-{}", std::process::id()));
161                let lease = lease::LeaseSettings { namespace: lease_namespace(), name: "rws-gateway-controller".to_string(), identity };
162                tokio::spawn(lease::run(client.clone(), lease, Arc::clone(&leader), Arc::clone(&became_leader)));
163            }
164        }
165        let plane = Arc::new(dataplane::DataPlane::new("0.0.0.0"));
166        let provisioned = settings.provision.is_some();
167        let on_config = move |mut config: translate::GatewayConfig| {
168            // Provisioned: each pod serves its own Gateway; the controller's serve none.
169            match &own {
170                Some(gateway) => config.listeners.retain(|l| &l.gateway == gateway),
171                None if provisioned => config.listeners.clear(),
172                None => {}
173            }
174            plane.apply(&config)
175        };
176        let options = controller::Options { settings, leader, on_config: Arc::new(on_config) };
177        controller::run(client, options, became_leader).await;
178    });
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn settings_parse() {
187        assert_eq!(BTreeMap::from([("a".to_string(), "1".to_string()), ("b".to_string(), "x".to_string())]), parse_labels(" a=1, b = x ,junk"));
188        assert_eq!(Some((10000, 10999)), parse_port_range("10000-10999"));
189        assert_eq!(None, parse_port_range("80-90"), "no privileged ports");
190        assert_eq!(None, parse_port_range("2000-1000"));
191        assert_eq!(None, parse_port_range("x"));
192    }
193}