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 addresses;
18pub mod backend_tls;
19pub mod controller;
20pub mod dataplane;
21pub mod ext_auth;
22pub mod filters;
23pub mod l4;
24pub mod lease;
25pub mod mesh;
26pub mod provision;
27pub mod resources;
28pub mod router;
29pub mod routes;
30pub mod services;
31pub mod translate;
32pub mod types;
33pub(crate) use crate::proxy::upgrade;
34
35/// The `controllerName` GatewayClasses name to be handled by rws.
36pub const CONTROLLER_NAME: &str = "rws8.tech/gateway-controller";
37
38/// The field manager rws writes status with.
39pub const FIELD_MANAGER: &str = "rws-gateway";
40
41use std::collections::BTreeMap;
42use std::sync::atomic::AtomicBool;
43use std::sync::Arc;
44
45/// `RWS_CONFIG_GATEWAY_API_ENABLED` (`[gateway_api] enabled`): run the
46/// controller and serve Gateways.
47pub const RWS_CONFIG_GATEWAY_API_ENABLED: &str = "RWS_CONFIG_GATEWAY_API_ENABLED";
48/// The `controllerName` handled (default [`CONTROLLER_NAME`]).
49pub const RWS_CONFIG_GATEWAY_API_CONTROLLER_NAME: &str = "RWS_CONFIG_GATEWAY_API_CONTROLLER_NAME";
50/// The labels of the pods Gateways' Services select, `key=value,…`.
51pub const RWS_CONFIG_GATEWAY_API_POD_SELECTOR: &str = "RWS_CONFIG_GATEWAY_API_POD_SELECTOR";
52/// Container ports to allocate, `first-last` (default `10000-10999`).
53pub const RWS_CONFIG_GATEWAY_API_PORT_RANGE: &str = "RWS_CONFIG_GATEWAY_API_PORT_RANGE";
54/// Gateways' Service type (default `LoadBalancer`).
55pub const RWS_CONFIG_GATEWAY_API_SERVICE_TYPE: &str = "RWS_CONFIG_GATEWAY_API_SERVICE_TYPE";
56/// Where the leader-election Lease lives (default: the pod's namespace).
57pub const RWS_CONFIG_GATEWAY_API_LEASE_NAMESPACE: &str = "RWS_CONFIG_GATEWAY_API_LEASE_NAMESPACE";
58/// `shared` (default: the controller's pods serve every Gateway) or
59/// `provisioned` (a Deployment per Gateway, [`provision`]).
60pub const RWS_CONFIG_GATEWAY_API_MODE: &str = "RWS_CONFIG_GATEWAY_API_MODE";
61/// The image provisioned Deployments run.
62pub const RWS_CONFIG_GATEWAY_API_IMAGE: &str = "RWS_CONFIG_GATEWAY_API_IMAGE";
63/// The ServiceAccount provisioned pods run as (default: the pod's, from
64/// `POD_SERVICE_ACCOUNT`).
65pub const RWS_CONFIG_GATEWAY_API_SERVICE_ACCOUNT: &str = "RWS_CONFIG_GATEWAY_API_SERVICE_ACCOUNT";
66/// Set on provisioned pods: `namespace/name` of the one Gateway they serve;
67/// they don't take the Lease or write anything.
68pub const RWS_CONFIG_GATEWAY_API_GATEWAY: &str = "RWS_CONFIG_GATEWAY_API_GATEWAY";
69
70/// `RWS_CONFIG_GATEWAY_API_EXPERIMENTAL` (`[gateway_api] experimental`): the
71/// cluster has the experimental-channel CRDs; rws handles and reports their
72/// fields (address routability, listener filters, ClusterTrustBundle).
73pub const RWS_CONFIG_GATEWAY_API_EXPERIMENTAL: &str = "RWS_CONFIG_GATEWAY_API_EXPERIMENTAL";
74/// `RWS_CONFIG_GATEWAY_API_MESH` (`[gateway_api] mesh`): the controller
75/// also handles routes attached to Services (the mesh profile, [`mesh`]) and
76/// serves sidecars their configuration.
77pub const RWS_CONFIG_GATEWAY_API_MESH: &str = "RWS_CONFIG_GATEWAY_API_MESH";
78/// The port of the mesh configuration endpoint (default 15010).
79pub const RWS_CONFIG_GATEWAY_API_MESH_PORT: &str = "RWS_CONFIG_GATEWAY_API_MESH_PORT";
80/// The controller's configuration endpoint as sidecars reach it, e.g.
81/// `http://rws-mesh.rws-system:15010`: a sidecar's source, and what the
82/// injection webhook gives injected sidecars.
83pub const RWS_CONFIG_GATEWAY_API_MESH_CONFIG_URL: &str = "RWS_CONFIG_GATEWAY_API_MESH_CONFIG_URL";
84/// The injection webhook's certificate and key (PEM files): with both set,
85/// the controller serves it ([`mesh::inject`]).
86pub const RWS_CONFIG_GATEWAY_API_MESH_WEBHOOK_CERT_FILE: &str = "RWS_CONFIG_GATEWAY_API_MESH_WEBHOOK_CERT_FILE";
87pub const RWS_CONFIG_GATEWAY_API_MESH_WEBHOOK_KEY_FILE: &str = "RWS_CONFIG_GATEWAY_API_MESH_WEBHOOK_KEY_FILE";
88/// The injection webhook's port (default 15017).
89pub const RWS_CONFIG_GATEWAY_API_MESH_WEBHOOK_PORT: &str = "RWS_CONFIG_GATEWAY_API_MESH_WEBHOOK_PORT";
90/// The image injected sidecars run (default: this release's rws image).
91pub const RWS_CONFIG_GATEWAY_API_MESH_SIDECAR_IMAGE: &str = "RWS_CONFIG_GATEWAY_API_MESH_SIDECAR_IMAGE";
92/// The image of the injected `rws-mesh-init` container.
93pub const RWS_CONFIG_GATEWAY_API_MESH_INIT_IMAGE: &str = "RWS_CONFIG_GATEWAY_API_MESH_INIT_IMAGE";
94
95fn setting(key: &str) -> Option<String> {
96    std::env::var(key).ok().map(|v| v.trim().to_string()).filter(|v| !v.is_empty())
97}
98
99fn on(key: &str) -> bool {
100    setting(key).is_some_and(|v| matches!(v.to_ascii_lowercase().as_str(), "true" | "1" | "yes" | "on"))
101}
102
103/// Whether the controller is turned on.
104pub fn enabled() -> bool {
105    on(RWS_CONFIG_GATEWAY_API_ENABLED)
106}
107
108/// Whether this process is a mesh sidecar (`RWS_CONFIG_GATEWAY_API_MODE=mesh-sidecar`).
109pub fn is_mesh_sidecar() -> bool {
110    setting(RWS_CONFIG_GATEWAY_API_MODE).is_some_and(|m| m.eq_ignore_ascii_case("mesh-sidecar"))
111}
112
113/// `key=value,key=value`.
114pub fn parse_labels(value: &str) -> BTreeMap<String, String> {
115    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()
116}
117
118/// `first-last`.
119pub fn parse_port_range(value: &str) -> Option<(u16, u16)> {
120    let (first, last) = value.split_once('-')?;
121    let (first, last) = (first.trim().parse::<u16>().ok()?, last.trim().parse::<u16>().ok()?);
122    (first >= 1024 && first <= last).then_some((first, last))
123}
124
125/// The translation settings, from `RWS_CONFIG_GATEWAY_API_*`.
126pub fn settings_from_env() -> translate::Settings {
127    let mut settings = translate::Settings::default();
128    if let Some(name) = setting(RWS_CONFIG_GATEWAY_API_CONTROLLER_NAME) {
129        settings.controller_name = name;
130    }
131    if let Some(selector) = setting(RWS_CONFIG_GATEWAY_API_POD_SELECTOR).map(|v| parse_labels(&v)).filter(|l| !l.is_empty()) {
132        settings.services.pod_selector = selector;
133    }
134    if let Some(range) = setting(RWS_CONFIG_GATEWAY_API_PORT_RANGE).and_then(|v| parse_port_range(&v)) {
135        settings.services.port_range = range;
136    }
137    if let Some(kind) = setting(RWS_CONFIG_GATEWAY_API_SERVICE_TYPE) {
138        settings.services.service_type = kind;
139    }
140    settings.services.pod_namespace = pod_namespace();
141    settings.mesh = on(RWS_CONFIG_GATEWAY_API_MESH);
142    settings.experimental = on(RWS_CONFIG_GATEWAY_API_EXPERIMENTAL);
143    if setting(RWS_CONFIG_GATEWAY_API_MODE).is_some_and(|m| m.eq_ignore_ascii_case("provisioned")) {
144        settings.services.provisioned = true;
145        settings.provision = Some(provision::ProvisionSettings {
146            image: setting(RWS_CONFIG_GATEWAY_API_IMAGE).unwrap_or_else(|| format!("ghcr.io/bohdaq/rws:{}", env!("CARGO_PKG_VERSION"))),
147            service_account: setting(RWS_CONFIG_GATEWAY_API_SERVICE_ACCOUNT).or_else(|| setting("POD_SERVICE_ACCOUNT")).unwrap_or_else(|| "default".to_string()),
148            ..Default::default()
149        });
150    }
151    settings
152}
153
154/// `namespace/name` from [`RWS_CONFIG_GATEWAY_API_GATEWAY`]: this pod is a
155/// provisioned data plane for that Gateway only.
156pub fn own_gateway() -> Option<(String, String)> {
157    let value = setting(RWS_CONFIG_GATEWAY_API_GATEWAY)?;
158    let (namespace, name) = value.split_once('/')?;
159    (!namespace.is_empty() && !name.is_empty()).then(|| (namespace.to_string(), name.to_string()))
160}
161
162/// The pod's namespace, where the serving pods run: `POD_NAMESPACE`, or
163/// the service account's.
164fn pod_namespace() -> String {
165    setting("POD_NAMESPACE")
166        .or_else(|| std::fs::read_to_string("/var/run/secrets/kubernetes.io/serviceaccount/namespace").ok().map(|n| n.trim().to_string()))
167        .unwrap_or_else(|| "default".to_string())
168}
169
170/// Where the Lease lives: the setting, else the pod's namespace.
171fn lease_namespace() -> String {
172    setting(RWS_CONFIG_GATEWAY_API_LEASE_NAMESPACE).unwrap_or_else(pod_namespace)
173}
174
175/// When enabled, start the controller, leader election and data plane on
176/// the current tokio runtime, against the cluster rws runs in (or the
177/// kubeconfig). Returns at once.
178pub fn start_from_env() {
179    if is_mesh_sidecar() {
180        let settings = mesh::sidecar::SidecarSettings {
181            config_url: setting(RWS_CONFIG_GATEWAY_API_MESH_CONFIG_URL).unwrap_or_else(|| format!("http://rws-mesh.rws-system:{}", mesh::config::DEFAULT_PORT)),
182            namespace: pod_namespace(),
183            outbound_port: mesh::sidecar::OUTBOUND_PORT,
184        };
185        tokio::spawn(mesh::sidecar::run(settings));
186        return;
187    }
188    if !enabled() {
189        return;
190    }
191    tokio::spawn(async {
192        let client = match kube::Client::try_default().await {
193            Ok(client) => client,
194            Err(e) => {
195                eprintln!("[gateway-api] no Kubernetes API: {e}");
196                return;
197            }
198        };
199        let settings = settings_from_env();
200        let leader = Arc::new(AtomicBool::new(false));
201        let became_leader = Arc::new(tokio::sync::Notify::new());
202        let own = own_gateway();
203        match &own {
204            // A provisioned data plane: never the leader.
205            Some((namespace, name)) => println!("[gateway-api] serving Gateway {namespace}/{name}"),
206            None => {
207                println!("[gateway-api] serving GatewayClasses with controllerName {}", settings.controller_name);
208                let identity = setting("POD_NAME").or_else(|| setting("HOSTNAME")).unwrap_or_else(|| format!("rws-{}", std::process::id()));
209                let lease = lease::LeaseSettings { namespace: lease_namespace(), name: "rws-gateway-controller".to_string(), identity };
210                tokio::spawn(lease::run(client.clone(), lease, Arc::clone(&leader), Arc::clone(&became_leader)));
211            }
212        }
213        let plane = Arc::new(dataplane::DataPlane::new("0.0.0.0"));
214        let provisioned = settings.provision.is_some();
215        let on_config = move |mut config: translate::GatewayConfig| {
216            // Provisioned: each pod serves its own Gateway; the controller's serve none.
217            match &own {
218                Some(gateway) => config.listeners.retain(|l| &l.gateway == gateway),
219                None if provisioned => config.listeners.clear(),
220                None => {}
221            }
222            plane.apply(&config)
223        };
224        let mesh = settings.mesh.then(|| {
225            let publisher = mesh::config::Publisher::new();
226            let port = setting(RWS_CONFIG_GATEWAY_API_MESH_PORT).and_then(|p| p.parse().ok()).unwrap_or(mesh::config::DEFAULT_PORT);
227            tokio::spawn(mesh::config::serve(publisher.clone(), port));
228            if let (Some(cert), Some(key)) = (setting(RWS_CONFIG_GATEWAY_API_MESH_WEBHOOK_CERT_FILE), setting(RWS_CONFIG_GATEWAY_API_MESH_WEBHOOK_KEY_FILE)) {
229                let version = env!("CARGO_PKG_VERSION");
230                let injector = mesh::inject::Injector {
231                    settings: mesh::inject::InjectSettings {
232                        sidecar_image: setting(RWS_CONFIG_GATEWAY_API_MESH_SIDECAR_IMAGE).unwrap_or_else(|| format!("ghcr.io/bohdaq/rws:{version}")),
233                        init_image: setting(RWS_CONFIG_GATEWAY_API_MESH_INIT_IMAGE).unwrap_or_else(|| format!("ghcr.io/bohdaq/rws-mesh-init:{version}")),
234                        config_url: setting(RWS_CONFIG_GATEWAY_API_MESH_CONFIG_URL).unwrap_or_else(|| format!("http://rws-mesh.{}:{port}", pod_namespace())),
235                    },
236                };
237                let webhook_port = setting(RWS_CONFIG_GATEWAY_API_MESH_WEBHOOK_PORT).and_then(|p| p.parse().ok()).unwrap_or(mesh::inject::DEFAULT_PORT);
238                tokio::spawn(mesh::inject::serve(injector, webhook_port, cert, key));
239            }
240            publisher
241        });
242        let options = controller::Options { settings, leader, on_config: Arc::new(on_config), mesh };
243        controller::run(client, options, became_leader).await;
244    });
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn settings_parse() {
253        assert_eq!(BTreeMap::from([("a".to_string(), "1".to_string()), ("b".to_string(), "x".to_string())]), parse_labels(" a=1, b = x ,junk"));
254        assert_eq!(Some((10000, 10999)), parse_port_range("10000-10999"));
255        assert_eq!(None, parse_port_range("80-90"), "no privileged ports");
256        assert_eq!(None, parse_port_range("2000-1000"));
257        assert_eq!(None, parse_port_range("x"));
258    }
259}