featherbit/config_store/
mod.rs1use async_trait::async_trait;
12use std::path::PathBuf;
13
14use crate::config::{load_yaml_with_env, GatewayConfig};
15use crate::state::SharedState;
16
17pub mod etcd;
18
19#[async_trait]
21pub trait ConfigStore: Send + Sync {
22 async fn load_all(&self) -> Result<GatewayConfig, String>;
24
25 async fn commit(&self, state: &SharedState, candidate: GatewayConfig) -> Result<(), String>;
33}
34
35pub struct FileConfigStore {
40 path: PathBuf,
41}
42
43impl FileConfigStore {
44 pub fn new(path: PathBuf) -> Self {
46 Self { path }
47 }
48}
49
50#[async_trait]
51impl ConfigStore for FileConfigStore {
52 async fn load_all(&self) -> Result<GatewayConfig, String> {
53 load_yaml_with_env(&self.path).map_err(|e| e.to_string())
54 }
55
56 async fn commit(&self, state: &SharedState, candidate: GatewayConfig) -> Result<(), String> {
57 state.apply_gateway(candidate).await
60 }
61}
62
63#[cfg(test)]
64pub mod tests {
65 use super::*;
66 use std::sync::Mutex;
67
68 pub struct FakeConfigStore {
72 pub initial: GatewayConfig,
73 pub committed: Mutex<Vec<GatewayConfig>>,
74 }
75
76 impl FakeConfigStore {
77 pub fn new(initial: GatewayConfig) -> Self {
78 Self {
79 initial,
80 committed: Mutex::new(Vec::new()),
81 }
82 }
83 }
84
85 #[async_trait]
86 impl ConfigStore for FakeConfigStore {
87 async fn load_all(&self) -> Result<GatewayConfig, String> {
88 Ok(self.initial.clone())
89 }
90
91 async fn commit(
92 &self,
93 state: &SharedState,
94 candidate: GatewayConfig,
95 ) -> Result<(), String> {
96 state.validate_gateway(&candidate)?;
98 self.committed.lock().unwrap().push(candidate);
99 Ok(())
100 }
101 }
102
103 use std::sync::Arc;
104
105 fn empty_state() -> Arc<SharedState> {
106 let system = serde_yaml::from_str("{}").unwrap();
107 let gateway = serde_yaml::from_str("{}").unwrap();
108 Arc::new(
109 SharedState::new(
110 system,
111 gateway,
112 None,
113 Arc::new(FakeConfigStore::new(serde_yaml::from_str("{}").unwrap())),
114 )
115 .unwrap(),
116 )
117 }
118
119 const VALID_GW: &str = r#"
120routes:
121 - name: r
122 match: { path: /api/* }
123 policy: p
124policies:
125 - name: p
126 nodes:
127 - { id: listener, type: listener }
128 - { id: client, type: client }
129 edges:
130 - { from: listener.out, to: client.in }
131"#;
132
133 #[tokio::test]
134 async fn test_file_commit_applies_valid_config() {
135 let state = empty_state();
136 let store = FileConfigStore::new("unused.yaml".into());
138 let candidate: GatewayConfig = serde_yaml::from_str(VALID_GW).unwrap();
139
140 store.commit(&state, candidate).await.unwrap();
141 assert_eq!(state.routes.read().await.len(), 1);
142 assert_eq!(state.gateway.read().await.routes[0].name, "r");
143 }
144
145 #[tokio::test]
146 async fn test_commit_rejects_invalid_config() {
147 let state = empty_state();
148 let bad: GatewayConfig = serde_yaml::from_str(
150 "routes:\n - name: r\n match: { path: /x }\n policy: missing\n",
151 )
152 .unwrap();
153
154 assert!(FileConfigStore::new("unused.yaml".into())
156 .commit(&state, bad.clone())
157 .await
158 .is_err());
159
160 let fake = FakeConfigStore::new(serde_yaml::from_str("{}").unwrap());
161 assert!(fake.commit(&state, bad).await.is_err());
162 assert!(fake.committed.lock().unwrap().is_empty());
163 assert_eq!(state.routes.read().await.len(), 0);
165 }
166}