Skip to main content

perfgate_server/
server.rs

1//! Server configuration and bootstrap.
2//!
3//! This module provides the [`ServerConfig`] and [`run_server`] function
4//! for starting the HTTP server.
5
6use 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/// Storage backend type.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum StorageBackend {
33    /// In-memory storage (for testing/development)
34    #[default]
35    Memory,
36    /// SQLite persistent storage
37    Sqlite,
38    /// PostgreSQL storage (not yet implemented)
39    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/// Server configuration.
56#[derive(Debug, Clone)]
57pub struct ServerConfig {
58    /// Bind address (e.g., "0.0.0.0:8080")
59    pub bind: SocketAddr,
60
61    /// Storage backend type
62    pub storage_backend: StorageBackend,
63
64    /// SQLite database path (when storage_backend is Sqlite)
65    pub sqlite_path: Option<PathBuf>,
66
67    /// PostgreSQL connection URL (when storage_backend is Postgres)
68    pub postgres_url: Option<String>,
69
70    /// API keys for authentication (key -> role mapping)
71    pub api_keys: Vec<(String, Role)>,
72
73    /// Optional JWT validation settings.
74    pub jwt: Option<JwtConfig>,
75
76    /// Enable CORS for all origins
77    pub cors: bool,
78
79    /// Request timeout in seconds
80    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    /// Creates a new configuration with default values.
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    /// Sets the bind address.
105    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    /// Sets the storage backend.
114    pub fn storage_backend(mut self, backend: StorageBackend) -> Self {
115        self.storage_backend = backend;
116        self
117    }
118
119    /// Sets the SQLite database path.
120    pub fn sqlite_path(mut self, path: impl Into<PathBuf>) -> Self {
121        self.sqlite_path = Some(path.into());
122        self
123    }
124
125    /// Sets the PostgreSQL connection URL.
126    pub fn postgres_url(mut self, url: impl Into<String>) -> Self {
127        self.postgres_url = Some(url.into());
128        self
129    }
130
131    /// Adds an API key with a specific role.
132    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    /// Enables JWT token authentication.
138    pub fn jwt(mut self, jwt: JwtConfig) -> Self {
139        self.jwt = Some(jwt);
140        self
141    }
142
143    /// Enables or disables CORS.
144    pub fn cors(mut self, enabled: bool) -> Self {
145        self.cors = enabled;
146        self
147    }
148}
149
150/// Creates the storage backend based on configuration.
151pub(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
180/// Creates the API key store from configuration.
181pub(crate) async fn create_key_store(
182    config: &ServerConfig,
183) -> Result<Arc<ApiKeyStore>, ConfigError> {
184    let store = ApiKeyStore::new();
185
186    // Add configured API keys
187    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
201/// Creates the router with all routes configured.
202pub(crate) fn create_router(
203    store: Arc<dyn BaselineStore>,
204    auth_state: AuthState,
205    config: &ServerConfig,
206) -> Router {
207    // Health check (no auth required)
208    let health_routes = Router::new().route("/health", get(health_check));
209
210    // API routes that require authentication
211    let api_routes = Router::new()
212        // Baseline CRUD
213        .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    // Combine routes under /api/v1, plus root /health
234    let mut app = Router::new()
235        .merge(health_routes.clone())
236        .nest("/api/v1", health_routes.merge(api_routes));
237
238    // Add CORS if enabled
239    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
251/// Runs the HTTP server.
252///
253/// This function starts the server and blocks until shutdown.
254pub 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    // Create storage
262    let store = create_storage(&config).await?;
263
264    // Create key store
265    let key_store = create_key_store(&config).await?;
266    let auth_state = AuthState::new(key_store, config.jwt.clone());
267
268    // Create router
269    let app = create_router(store.clone(), auth_state, &config);
270
271    // Add tracing and request ID layers
272    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    // Create listener
281    let listener = tokio::net::TcpListener::bind(config.bind).await?;
282    info!(addr = %config.bind, "Server listening");
283
284    // Run server with graceful shutdown
285    axum::serve(listener, app)
286        .with_graceful_shutdown(shutdown_signal())
287        .await?;
288
289    info!("Server shutdown complete");
290    Ok(())
291}
292
293/// Creates a shutdown signal handler.
294async 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        // Router created successfully
416    }
417}