1use std::net::SocketAddr;
7use std::path::PathBuf;
8use std::sync::Arc;
9
10use axum::{
11 Router, middleware,
12 routing::{delete, get, post},
13};
14use tower::ServiceBuilder;
15use tower_http::{
16 cors::{Any, CorsLayer},
17 request_id::MakeRequestUuid,
18 trace::TraceLayer,
19};
20use tracing::info;
21
22use crate::auth::{ApiKey, ApiKeyStore, AuthState, JwtConfig, Role, auth_middleware};
23use crate::error::ConfigError;
24use crate::handlers::{
25 delete_baseline, get_baseline, get_latest_baseline, health_check, list_baselines,
26 promote_baseline, upload_baseline,
27};
28use crate::storage::{BaselineStore, InMemoryStore, PostgresStore, SqliteStore};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum StorageBackend {
33 #[default]
35 Memory,
36 Sqlite,
38 Postgres,
40}
41
42impl std::str::FromStr for StorageBackend {
43 type Err = String;
44
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 match s.to_lowercase().as_str() {
47 "memory" => Ok(Self::Memory),
48 "sqlite" => Ok(Self::Sqlite),
49 "postgres" | "postgresql" => Ok(Self::Postgres),
50 _ => Err(format!("Unknown storage backend: {}", s)),
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct ServerConfig {
58 pub bind: SocketAddr,
60
61 pub storage_backend: StorageBackend,
63
64 pub sqlite_path: Option<PathBuf>,
66
67 pub postgres_url: Option<String>,
69
70 pub api_keys: Vec<(String, Role)>,
72
73 pub jwt: Option<JwtConfig>,
75
76 pub cors: bool,
78
79 pub timeout_seconds: u64,
81}
82
83impl Default for ServerConfig {
84 fn default() -> Self {
85 Self {
86 bind: "0.0.0.0:8080".parse().unwrap(),
87 storage_backend: StorageBackend::Memory,
88 sqlite_path: None,
89 postgres_url: None,
90 api_keys: vec![],
91 jwt: None,
92 cors: true,
93 timeout_seconds: 30,
94 }
95 }
96}
97
98impl ServerConfig {
99 pub fn new() -> Self {
101 Self::default()
102 }
103
104 pub fn bind(mut self, addr: impl Into<String>) -> Result<Self, ConfigError> {
106 self.bind = addr
107 .into()
108 .parse()
109 .map_err(|e| ConfigError::InvalidValue(format!("Invalid bind address: {}", e)))?;
110 Ok(self)
111 }
112
113 pub fn storage_backend(mut self, backend: StorageBackend) -> Self {
115 self.storage_backend = backend;
116 self
117 }
118
119 pub fn sqlite_path(mut self, path: impl Into<PathBuf>) -> Self {
121 self.sqlite_path = Some(path.into());
122 self
123 }
124
125 pub fn postgres_url(mut self, url: impl Into<String>) -> Self {
127 self.postgres_url = Some(url.into());
128 self
129 }
130
131 pub fn api_key(mut self, key: impl Into<String>, role: Role) -> Self {
133 self.api_keys.push((key.into(), role));
134 self
135 }
136
137 pub fn jwt(mut self, jwt: JwtConfig) -> Self {
139 self.jwt = Some(jwt);
140 self
141 }
142
143 pub fn cors(mut self, enabled: bool) -> Self {
145 self.cors = enabled;
146 self
147 }
148}
149
150pub(crate) async fn create_storage(
152 config: &ServerConfig,
153) -> Result<Arc<dyn BaselineStore>, ConfigError> {
154 match config.storage_backend {
155 StorageBackend::Memory => {
156 info!("Using in-memory storage");
157 Ok(Arc::new(InMemoryStore::new()))
158 }
159 StorageBackend::Sqlite => {
160 let path = config
161 .sqlite_path
162 .clone()
163 .unwrap_or_else(|| PathBuf::from("perfgate.db"));
164 info!(path = %path.display(), "Using SQLite storage");
165 let store = SqliteStore::new(&path)
166 .map_err(|e| ConfigError::InvalidValue(format!("Failed to open SQLite: {}", e)))?;
167 Ok(Arc::new(store))
168 }
169 StorageBackend::Postgres => {
170 let url = config
171 .postgres_url
172 .clone()
173 .unwrap_or_else(|| "postgres://localhost:5432/perfgate".to_string());
174 info!(url = %url, "Using PostgreSQL storage (stub)");
175 Ok(Arc::new(PostgresStore::new(&url)))
176 }
177 }
178}
179
180pub(crate) async fn create_key_store(
182 config: &ServerConfig,
183) -> Result<Arc<ApiKeyStore>, ConfigError> {
184 let store = ApiKeyStore::new();
185
186 for (key, role) in &config.api_keys {
188 let api_key = ApiKey::new(
189 uuid::Uuid::new_v4().to_string(),
190 format!("{:?} key", role),
191 "default".to_string(),
192 *role,
193 );
194 store.add_key(api_key, key).await;
195 info!(role = ?role, "Added API key");
196 }
197
198 Ok(Arc::new(store))
199}
200
201pub(crate) fn create_router(
203 store: Arc<dyn BaselineStore>,
204 auth_state: AuthState,
205 config: &ServerConfig,
206) -> Router {
207 let health_routes = Router::new().route("/health", get(health_check));
209
210 let api_routes = Router::new()
212 .route("/projects/{project}/baselines", post(upload_baseline))
214 .route(
215 "/projects/{project}/baselines/{benchmark}/latest",
216 get(get_latest_baseline),
217 )
218 .route(
219 "/projects/{project}/baselines/{benchmark}/versions/{version}",
220 get(get_baseline),
221 )
222 .route(
223 "/projects/{project}/baselines/{benchmark}/versions/{version}",
224 delete(delete_baseline),
225 )
226 .route("/projects/{project}/baselines", get(list_baselines))
227 .route(
228 "/projects/{project}/baselines/{benchmark}/promote",
229 post(promote_baseline),
230 )
231 .layer(middleware::from_fn_with_state(auth_state, auth_middleware));
232
233 let mut app = Router::new()
235 .merge(health_routes.clone())
236 .nest("/api/v1", health_routes.merge(api_routes));
237
238 if config.cors {
240 app = app.layer(
241 CorsLayer::new()
242 .allow_origin(Any)
243 .allow_methods(Any)
244 .allow_headers(Any),
245 );
246 }
247
248 app.with_state(store)
249}
250
251pub async fn run_server(config: ServerConfig) -> Result<(), Box<dyn std::error::Error>> {
255 info!(
256 bind = %config.bind,
257 backend = ?config.storage_backend,
258 "Starting perfgate server"
259 );
260
261 let store = create_storage(&config).await?;
263
264 let key_store = create_key_store(&config).await?;
266 let auth_state = AuthState::new(key_store, config.jwt.clone());
267
268 let app = create_router(store.clone(), auth_state, &config);
270
271 let app = app.layer(
273 ServiceBuilder::new()
274 .layer(TraceLayer::new_for_http())
275 .layer(tower_http::request_id::SetRequestIdLayer::x_request_id(
276 MakeRequestUuid,
277 )),
278 );
279
280 let listener = tokio::net::TcpListener::bind(config.bind).await?;
282 info!(addr = %config.bind, "Server listening");
283
284 axum::serve(listener, app)
286 .with_graceful_shutdown(shutdown_signal())
287 .await?;
288
289 info!("Server shutdown complete");
290 Ok(())
291}
292
293async fn shutdown_signal() {
295 use tokio::signal;
296
297 let ctrl_c = async {
298 signal::ctrl_c()
299 .await
300 .expect("Failed to install Ctrl+C handler");
301 };
302
303 #[cfg(unix)]
304 let terminate = async {
305 signal::unix::signal(signal::unix::SignalKind::terminate())
306 .expect("Failed to install signal handler")
307 .recv()
308 .await;
309 };
310
311 #[cfg(not(unix))]
312 let terminate = std::future::pending::<()>();
313
314 tokio::select! {
315 _ = ctrl_c => {},
316 _ = terminate => {},
317 }
318
319 info!("Shutdown signal received");
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_server_config_default() {
328 let config = ServerConfig::new();
329 assert_eq!(config.bind.to_string(), "0.0.0.0:8080");
330 assert_eq!(config.storage_backend, StorageBackend::Memory);
331 }
332
333 #[test]
334 fn test_server_config_builder() {
335 let config = ServerConfig::new()
336 .bind("127.0.0.1:3000")
337 .unwrap()
338 .storage_backend(StorageBackend::Sqlite)
339 .sqlite_path("/tmp/test.db")
340 .api_key("test-key", Role::Admin)
341 .jwt(JwtConfig::hs256(b"test-secret".to_vec()).issuer("perfgate"))
342 .cors(false);
343
344 assert_eq!(config.bind.to_string(), "127.0.0.1:3000");
345 assert_eq!(config.storage_backend, StorageBackend::Sqlite);
346 assert_eq!(config.sqlite_path, Some(PathBuf::from("/tmp/test.db")));
347 assert_eq!(config.api_keys.len(), 1);
348 assert!(config.jwt.is_some());
349 assert!(!config.cors);
350 }
351
352 #[test]
353 fn test_storage_backend_from_str() {
354 assert_eq!(
355 "memory".parse::<StorageBackend>().unwrap(),
356 StorageBackend::Memory
357 );
358 assert_eq!(
359 "sqlite".parse::<StorageBackend>().unwrap(),
360 StorageBackend::Sqlite
361 );
362 assert_eq!(
363 "postgres".parse::<StorageBackend>().unwrap(),
364 StorageBackend::Postgres
365 );
366 assert!("invalid".parse::<StorageBackend>().is_err());
367 }
368
369 #[tokio::test]
370 async fn test_create_storage_memory() {
371 let config = ServerConfig::new().storage_backend(StorageBackend::Memory);
372 let storage = create_storage(&config).await.unwrap();
373 assert_eq!(storage.backend_type(), "memory");
374 }
375
376 #[tokio::test(flavor = "multi_thread")]
377 async fn test_create_storage_sqlite() {
378 let config = ServerConfig::new()
379 .storage_backend(StorageBackend::Sqlite)
380 .sqlite_path(":memory:");
381 let storage = create_storage(&config).await.unwrap();
382 assert_eq!(storage.backend_type(), "sqlite");
383 }
384
385 #[tokio::test]
386 async fn test_create_storage_postgres() {
387 let config = ServerConfig::new()
388 .storage_backend(StorageBackend::Postgres)
389 .postgres_url("postgresql://localhost/test");
390 let result = create_storage(&config).await;
391 assert!(result.is_ok());
392 let store = result.unwrap();
393 assert_eq!(store.backend_type(), "postgres");
394 }
395
396 #[tokio::test]
397 async fn test_create_key_store() {
398 let config = ServerConfig::new()
399 .api_key("pg_live_test123456789012345678901234567890", Role::Admin)
400 .api_key("pg_live_viewer123456789012345678901234567", Role::Viewer);
401
402 let key_store = create_key_store(&config).await.unwrap();
403 let keys = key_store.list_keys().await;
404
405 assert_eq!(keys.len(), 2);
406 }
407
408 #[tokio::test]
409 async fn test_router_creation() {
410 let store = Arc::new(InMemoryStore::new());
411 let auth_state = AuthState::new(Arc::new(ApiKeyStore::new()), None);
412 let config = ServerConfig::new();
413
414 let _router = create_router(store, auth_state, &config);
415 }
417}