featherbit/config_store/
mod.rs1use async_trait::async_trait;
12use std::path::PathBuf;
13
14use crate::config::{load_yaml, 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(&self.path).map_err(|e| e.to_string())
57 }
58
59 async fn commit(&self, state: &SharedState, candidate: GatewayConfig) -> Result<(), String> {
60 state.apply_gateway(candidate).await
63 }
64}
65
66#[cfg(test)]
67pub mod tests {
68 use super::*;
69 use std::sync::Mutex;
70
71 pub struct FakeConfigStore {
75 pub initial: GatewayConfig,
76 pub committed: Mutex<Vec<GatewayConfig>>,
77 }
78
79 impl FakeConfigStore {
80 pub fn new(initial: GatewayConfig) -> Self {
81 Self {
82 initial,
83 committed: Mutex::new(Vec::new()),
84 }
85 }
86 }
87
88 #[async_trait]
89 impl ConfigStore for FakeConfigStore {
90 async fn load_all(&self) -> Result<GatewayConfig, String> {
91 Ok(self.initial.clone())
92 }
93
94 async fn commit(
95 &self,
96 state: &SharedState,
97 candidate: GatewayConfig,
98 ) -> Result<(), String> {
99 state.validate_gateway(&candidate)?;
101 self.committed.lock().unwrap().push(candidate);
102 Ok(())
103 }
104 }
105
106 use std::sync::Arc;
107
108 fn empty_state() -> Arc<SharedState> {
109 let system = serde_yaml::from_str("{}").unwrap();
110 let gateway = serde_yaml::from_str("{}").unwrap();
111 Arc::new(
112 SharedState::new(
113 system,
114 gateway,
115 None,
116 Arc::new(FakeConfigStore::new(serde_yaml::from_str("{}").unwrap())),
117 )
118 .unwrap(),
119 )
120 }
121
122 const VALID_GW: &str = r#"
123routes:
124 - name: r
125 match: { path: /api/* }
126 policy: p
127policies:
128 - name: p
129 nodes:
130 - { id: listener, type: listener }
131 - { id: client, type: client }
132 edges:
133 - { from: listener.out, to: client.in }
134"#;
135
136 #[tokio::test]
137 async fn test_load_all_preserves_env_placeholders() {
138 std::env::set_var("TEST_STORE_SECRET", "actual-secret-value");
144 let path = std::env::temp_dir().join(format!("fb_store_raw_{}.yaml", std::process::id()));
145 std::fs::write(
146 &path,
147 r#"
148policies:
149 - name: p
150 nodes:
151 - id: auth
152 type: openid-connect
153 config:
154 client_secret: ${TEST_STORE_SECRET}
155"#,
156 )
157 .unwrap();
158
159 let gw = FileConfigStore::new(path.clone()).load_all().await.unwrap();
160 assert_eq!(
161 gw.policies[0].nodes[0].config["client_secret"],
162 serde_json::json!("${TEST_STORE_SECRET}")
163 );
164
165 std::fs::remove_file(&path).ok();
166 std::env::remove_var("TEST_STORE_SECRET");
167 }
168
169 #[tokio::test]
170 async fn test_file_commit_applies_valid_config() {
171 let state = empty_state();
172 let store = FileConfigStore::new("unused.yaml".into());
174 let candidate: GatewayConfig = serde_yaml::from_str(VALID_GW).unwrap();
175
176 store.commit(&state, candidate).await.unwrap();
177 assert_eq!(state.routes.read().await.len(), 1);
178 assert_eq!(state.gateway.read().await.routes[0].name, "r");
179 }
180
181 #[tokio::test]
182 async fn test_commit_rejects_invalid_config() {
183 let state = empty_state();
184 let bad: GatewayConfig = serde_yaml::from_str(
186 "routes:\n - name: r\n match: { path: /x }\n policy: missing\n",
187 )
188 .unwrap();
189
190 assert!(FileConfigStore::new("unused.yaml".into())
192 .commit(&state, bad.clone())
193 .await
194 .is_err());
195
196 let fake = FakeConfigStore::new(serde_yaml::from_str("{}").unwrap());
197 assert!(fake.commit(&state, bad).await.is_err());
198 assert!(fake.committed.lock().unwrap().is_empty());
199 assert_eq!(state.routes.read().await.len(), 0);
201 }
202}