rws_gateway_api/gateway_api/
mod.rs1pub 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
35pub const CONTROLLER_NAME: &str = "rws8.tech/gateway-controller";
37
38pub const FIELD_MANAGER: &str = "rws-gateway";
40
41use std::collections::BTreeMap;
42use std::sync::atomic::AtomicBool;
43use std::sync::Arc;
44
45pub const RWS_CONFIG_GATEWAY_API_ENABLED: &str = "RWS_CONFIG_GATEWAY_API_ENABLED";
48pub const RWS_CONFIG_GATEWAY_API_CONTROLLER_NAME: &str = "RWS_CONFIG_GATEWAY_API_CONTROLLER_NAME";
50pub const RWS_CONFIG_GATEWAY_API_POD_SELECTOR: &str = "RWS_CONFIG_GATEWAY_API_POD_SELECTOR";
52pub const RWS_CONFIG_GATEWAY_API_PORT_RANGE: &str = "RWS_CONFIG_GATEWAY_API_PORT_RANGE";
54pub const RWS_CONFIG_GATEWAY_API_SERVICE_TYPE: &str = "RWS_CONFIG_GATEWAY_API_SERVICE_TYPE";
56pub const RWS_CONFIG_GATEWAY_API_LEASE_NAMESPACE: &str = "RWS_CONFIG_GATEWAY_API_LEASE_NAMESPACE";
58pub const RWS_CONFIG_GATEWAY_API_MODE: &str = "RWS_CONFIG_GATEWAY_API_MODE";
61pub const RWS_CONFIG_GATEWAY_API_IMAGE: &str = "RWS_CONFIG_GATEWAY_API_IMAGE";
63pub const RWS_CONFIG_GATEWAY_API_SERVICE_ACCOUNT: &str = "RWS_CONFIG_GATEWAY_API_SERVICE_ACCOUNT";
66pub const RWS_CONFIG_GATEWAY_API_GATEWAY: &str = "RWS_CONFIG_GATEWAY_API_GATEWAY";
69
70pub const RWS_CONFIG_GATEWAY_API_EXPERIMENTAL: &str = "RWS_CONFIG_GATEWAY_API_EXPERIMENTAL";
74pub const RWS_CONFIG_GATEWAY_API_MESH: &str = "RWS_CONFIG_GATEWAY_API_MESH";
78pub const RWS_CONFIG_GATEWAY_API_MESH_PORT: &str = "RWS_CONFIG_GATEWAY_API_MESH_PORT";
80pub const RWS_CONFIG_GATEWAY_API_MESH_CONFIG_URL: &str = "RWS_CONFIG_GATEWAY_API_MESH_CONFIG_URL";
84pub 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";
88pub const RWS_CONFIG_GATEWAY_API_MESH_WEBHOOK_PORT: &str = "RWS_CONFIG_GATEWAY_API_MESH_WEBHOOK_PORT";
90pub const RWS_CONFIG_GATEWAY_API_MESH_SIDECAR_IMAGE: &str = "RWS_CONFIG_GATEWAY_API_MESH_SIDECAR_IMAGE";
92pub 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
103pub fn enabled() -> bool {
105 on(RWS_CONFIG_GATEWAY_API_ENABLED)
106}
107
108pub 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
113pub 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
118pub 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
125pub 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
154pub 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
162fn 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
170fn lease_namespace() -> String {
172 setting(RWS_CONFIG_GATEWAY_API_LEASE_NAMESPACE).unwrap_or_else(pod_namespace)
173}
174
175pub 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 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 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}