1use std::path::PathBuf;
22use std::sync::Arc;
23use std::time::Duration;
24
25use async_trait::async_trait;
26use base64::engine::general_purpose::STANDARD as BASE64;
27use base64::Engine;
28use bytes::Bytes;
29use serde_json::{json, Value};
30use tokio::sync::Mutex;
31
32use crate::config::{EtcdConfig, GatewayConfig, SystemConfig};
33use crate::config::{PolicyConfig, RouteConfig};
34use crate::config_store::ConfigStore;
35use crate::consumers::ConsumerConfig;
36use crate::outbound::{OutboundClient, OutboundRequest};
37use crate::state::SharedState;
38
39pub struct EtcdConfigStore {
41 client: Arc<OutboundClient>,
42 endpoints: Vec<String>,
44 prefix: String,
46 auth: Option<(String, String)>,
47 token: Mutex<Option<String>>,
49 timeout: Duration,
50}
51
52impl EtcdConfigStore {
53 pub fn new(cfg: &EtcdConfig) -> Self {
55 Self {
56 client: Arc::new(OutboundClient::new()),
57 endpoints: cfg.endpoints.clone(),
58 prefix: cfg.prefix.trim_end_matches('/').to_string(),
59 auth: match (&cfg.user, &cfg.password) {
60 (Some(u), Some(p)) => Some((u.clone(), p.clone())),
61 _ => None,
62 },
63 token: Mutex::new(None),
64 timeout: Duration::from_millis(cfg.timeout_ms),
65 }
66 }
67
68 fn base(&self) -> Result<&str, String> {
69 self.endpoints
70 .first()
71 .map(String::as_str)
72 .ok_or_else(|| "no etcd endpoints configured".to_string())
73 }
74
75 async fn call(&self, path: &str, body: &Value) -> Result<Value, String> {
78 match self.call_once(path, body).await {
79 Err(EtcdCallError::Unauthorized) if self.auth.is_some() => {
80 self.authenticate().await?;
81 self.call_once(path, body).await.map_err(|e| e.to_string())
82 }
83 other => other.map_err(|e| e.to_string()),
84 }
85 }
86
87 async fn call_once(&self, path: &str, body: &Value) -> Result<Value, EtcdCallError> {
88 let url = format!("{}{}", self.base().map_err(EtcdCallError::Other)?, path);
89 let mut headers = vec![("content-type".to_string(), "application/json".to_string())];
90 if let Some(token) = self.token.lock().await.clone() {
91 headers.push(("authorization".to_string(), token));
92 }
93 let req = OutboundRequest {
94 method: http::Method::POST,
95 url,
96 headers,
97 body: Bytes::from(serde_json::to_vec(body).unwrap_or_default()),
98 timeout: self.timeout,
99 ssl_verify: true,
100 tls: None,
101 };
102 let resp = self
103 .client
104 .request(req)
105 .await
106 .map_err(|e| EtcdCallError::Other(e.to_string()))?;
107 if resp.status == 401 {
108 return Err(EtcdCallError::Unauthorized);
109 }
110 if resp.status != 200 {
111 return Err(EtcdCallError::Other(format!(
112 "etcd {} returned status {}: {}",
113 path,
114 resp.status,
115 String::from_utf8_lossy(&resp.body)
116 )));
117 }
118 serde_json::from_slice(&resp.body)
119 .map_err(|e| EtcdCallError::Other(format!("invalid etcd response: {}", e)))
120 }
121
122 async fn authenticate(&self) -> Result<(), String> {
124 let (user, pass) = match &self.auth {
125 Some(c) => c,
126 None => return Ok(()),
127 };
128 let resp = self
129 .call_once(
130 "/v3/auth/authenticate",
131 &json!({ "name": user, "password": pass }),
132 )
133 .await
134 .map_err(|e| e.to_string())?;
135 let token = resp
136 .get("token")
137 .and_then(|v| v.as_str())
138 .ok_or("etcd auth response missing token")?;
139 *self.token.lock().await = Some(token.to_string());
140 Ok(())
141 }
142
143 async fn range_prefix(&self) -> Result<Vec<(String, Vec<u8>)>, String> {
145 let key = format!("{}/", self.prefix);
146 let range_end = prefix_range_end(key.as_bytes());
147 let resp = self
148 .call(
149 "/v3/kv/range",
150 &json!({
151 "key": BASE64.encode(key.as_bytes()),
152 "range_end": BASE64.encode(range_end),
153 }),
154 )
155 .await?;
156 let mut out = Vec::new();
157 if let Some(kvs) = resp.get("kvs").and_then(|v| v.as_array()) {
158 for kv in kvs {
159 let k = kv.get("key").and_then(|v| v.as_str()).unwrap_or("");
160 let v = kv.get("value").and_then(|v| v.as_str()).unwrap_or("");
161 let key = BASE64
162 .decode(k)
163 .ok()
164 .and_then(|b| String::from_utf8(b).ok())
165 .ok_or("etcd key not valid base64/utf8")?;
166 let value = BASE64
167 .decode(v)
168 .map_err(|_| "etcd value not valid base64")?;
169 out.push((key, value));
170 }
171 }
172 Ok(out)
173 }
174
175 async fn put(&self, key: &str, value: &[u8]) -> Result<(), String> {
176 self.call(
177 "/v3/kv/put",
178 &json!({ "key": BASE64.encode(key.as_bytes()), "value": BASE64.encode(value) }),
179 )
180 .await
181 .map(|_| ())
182 }
183
184 async fn delete(&self, key: &str) -> Result<(), String> {
185 self.call(
186 "/v3/kv/deleterange",
187 &json!({ "key": BASE64.encode(key.as_bytes()) }),
188 )
189 .await
190 .map(|_| ())
191 }
192
193 fn route_key(&self, name: &str) -> String {
194 format!("{}/routes/{}", self.prefix, name)
195 }
196 fn policy_key(&self, name: &str) -> String {
197 format!("{}/policies/{}", self.prefix, name)
198 }
199 fn consumer_key(&self, name: &str) -> String {
200 format!("{}/consumers/{}", self.prefix, name)
201 }
202
203 async fn write_all(&self, gw: &GatewayConfig) -> Result<(), String> {
205 for r in &gw.routes {
206 self.put(&self.route_key(&r.name), &serde_json::to_vec(r).unwrap())
207 .await?;
208 }
209 for p in &gw.policies {
210 self.put(&self.policy_key(&p.name), &serde_json::to_vec(p).unwrap())
211 .await?;
212 }
213 for c in &gw.consumers {
214 self.put(&self.consumer_key(&c.name), &serde_json::to_vec(c).unwrap())
215 .await?;
216 }
217 Ok(())
218 }
219}
220
221#[async_trait]
222impl ConfigStore for EtcdConfigStore {
223 async fn load_all(&self) -> Result<GatewayConfig, String> {
224 let kvs = self.range_prefix().await?;
225 gateway_from_kvs(&self.prefix, kvs)
226 }
227
228 async fn commit(&self, state: &SharedState, candidate: GatewayConfig) -> Result<(), String> {
229 state.validate_gateway(&candidate)?;
231
232 let current: std::collections::HashSet<String> = self
235 .range_prefix()
236 .await?
237 .into_iter()
238 .map(|(k, _)| k)
239 .collect();
240 let mut desired = std::collections::HashSet::new();
241
242 for r in &candidate.routes {
243 let key = self.route_key(&r.name);
244 self.put(&key, &serde_json::to_vec(r).unwrap()).await?;
245 desired.insert(key);
246 }
247 for p in &candidate.policies {
248 let key = self.policy_key(&p.name);
249 self.put(&key, &serde_json::to_vec(p).unwrap()).await?;
250 desired.insert(key);
251 }
252 for c in &candidate.consumers {
253 let key = self.consumer_key(&c.name);
254 self.put(&key, &serde_json::to_vec(c).unwrap()).await?;
255 desired.insert(key);
256 }
257 for stale in current.difference(&desired) {
258 self.delete(stale).await?;
259 }
260
261 state.apply_gateway(candidate).await
264 }
265}
266
267fn gateway_from_kvs(prefix: &str, kvs: Vec<(String, Vec<u8>)>) -> Result<GatewayConfig, String> {
273 let mut gw = GatewayConfig {
274 routes: Vec::new(),
275 policies: Vec::new(),
276 consumers: Vec::new(),
277 };
278 for (key, value) in kvs {
279 let rest = match key.strip_prefix(&format!("{}/", prefix)) {
280 Some(r) => r,
281 None => continue,
282 };
283 let (category, _name) = match rest.split_once('/') {
284 Some(p) => p,
285 None => continue,
286 };
287 match category {
288 "routes" => {
289 let r: RouteConfig = serde_json::from_slice(&value)
290 .map_err(|e| format!("bad route '{}': {}", key, e))?;
291 gw.routes.push(r);
292 }
293 "policies" => {
294 let p: PolicyConfig = serde_json::from_slice(&value)
295 .map_err(|e| format!("bad policy '{}': {}", key, e))?;
296 gw.policies.push(p);
297 }
298 "consumers" => {
299 let c: ConsumerConfig = serde_json::from_slice(&value)
300 .map_err(|e| format!("bad consumer '{}': {}", key, e))?;
301 gw.consumers.push(c);
302 }
303 _ => {}
304 }
305 }
306 Ok(gw)
307}
308
309fn prefix_range_end(prefix: &[u8]) -> Vec<u8> {
313 let mut end = prefix.to_vec();
314 while let Some(&last) = end.last() {
315 if last < 0xff {
316 *end.last_mut().unwrap() = last + 1;
317 return end;
318 }
319 end.pop();
320 }
321 vec![0]
322}
323
324fn is_empty(gw: &GatewayConfig) -> bool {
325 gw.routes.is_empty() && gw.policies.is_empty() && gw.consumers.is_empty()
326}
327
328pub async fn build_source(
334 system: &SystemConfig,
335 seed_path: &std::path::Path,
336) -> Result<(Arc<dyn ConfigStore>, GatewayConfig, Option<PathBuf>), String> {
337 let cfg = system
338 .config
339 .etcd
340 .as_ref()
341 .ok_or("config.source is 'etcd' but no 'config.etcd' block is set")?;
342 let store = Arc::new(EtcdConfigStore::new(cfg));
343 if store.auth.is_some() {
344 store.authenticate().await?;
345 }
346
347 let mut gateway = store.load_all().await?;
348 if is_empty(&gateway) {
349 if let Ok(local) = crate::config::load_yaml_with_env::<GatewayConfig>(seed_path) {
350 if !is_empty(&local) {
351 tracing::info!("etcd prefix empty — seeding from {}", seed_path.display());
352 store.write_all(&local).await?;
353 gateway = store.load_all().await?;
354 }
355 }
356 }
357
358 let store: Arc<dyn ConfigStore> = store;
359 Ok((store, gateway, None))
360}
361
362pub fn spawn_watch(state: Arc<SharedState>, system: &SystemConfig) {
367 let interval = Duration::from_secs(2);
368 let store = state.config_store.clone();
369 tokio::spawn(async move {
370 let mut last: Option<String> = None;
371 loop {
372 tokio::time::sleep(interval).await;
373 match store.load_all().await {
374 Ok(gw) => {
375 let fingerprint = serde_json::to_string(&gw).unwrap_or_default();
376 if last.as_deref() != Some(fingerprint.as_str()) {
377 match state.apply_gateway(gw).await {
378 Ok(_) => last = Some(fingerprint),
379 Err(e) => tracing::error!("etcd config apply failed: {}", e),
380 }
381 }
382 }
383 Err(e) => tracing::warn!("etcd poll failed (keeping last-good config): {}", e),
384 }
385 }
386 });
387 let _ = system; }
389
390enum EtcdCallError {
393 Unauthorized,
394 Other(String),
395}
396
397impl std::fmt::Display for EtcdCallError {
398 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399 match self {
400 Self::Unauthorized => write!(f, "etcd unauthorized"),
401 Self::Other(m) => write!(f, "{}", m),
402 }
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn test_prefix_range_end() {
412 assert_eq!(prefix_range_end(b"/featherbit/"), b"/featherbit0".to_vec()); assert_eq!(prefix_range_end(b"ab"), b"ac".to_vec());
414 assert_eq!(prefix_range_end(&[0xff, 0xff]), vec![0]);
415 assert_eq!(prefix_range_end(&[0x01, 0xff]), vec![0x02]);
416 }
417
418 #[test]
419 fn test_gateway_from_kvs() {
420 let prefix = "/featherbit";
421 let route = serde_json::to_vec(&json!({
422 "name": "r", "match": { "path": "/api/*" }, "policy": "p"
423 }))
424 .unwrap();
425 let policy = serde_json::to_vec(&json!({
426 "name": "p",
427 "nodes": [{ "id": "listener", "type": "listener" }, { "id": "client", "type": "client" }],
428 "edges": [{ "from": "listener.out", "to": "client.in" }]
429 }))
430 .unwrap();
431 let consumer = serde_json::to_vec(&json!({ "name": "alice" })).unwrap();
432
433 let kvs = vec![
434 ("/featherbit/routes/r".to_string(), route),
435 ("/featherbit/policies/p".to_string(), policy),
436 ("/featherbit/consumers/alice".to_string(), consumer),
437 ("/featherbit/unknown/x".to_string(), b"{}".to_vec()), ("/other/routes/z".to_string(), b"{}".to_vec()), ];
440 let gw = gateway_from_kvs(prefix, kvs).unwrap();
441 assert_eq!(gw.routes.len(), 1);
442 assert_eq!(gw.routes[0].name, "r");
443 assert_eq!(gw.policies.len(), 1);
444 assert_eq!(gw.consumers.len(), 1);
445 assert_eq!(gw.consumers[0].name, "alice");
446 }
447
448 #[test]
449 fn test_gateway_from_kvs_rejects_bad_json() {
450 let kvs = vec![("/featherbit/routes/r".to_string(), b"not json".to_vec())];
451 assert!(gateway_from_kvs("/featherbit", kvs).is_err());
452 }
453
454 #[test]
455 fn test_is_empty() {
456 assert!(is_empty(&GatewayConfig {
457 routes: vec![],
458 policies: vec![],
459 consumers: vec![]
460 }));
461 }
462}