llm_config_api/
server.rs

1//! HTTP server implementation
2
3use crate::middleware::{comprehensive_security_middleware, SecurityState};
4use crate::routes::{
5    delete_config, get_config, get_history, health_check, list_configs, rollback_config,
6    set_config, ApiState,
7};
8use axum::{
9    middleware,
10    routing::{delete, get, post},
11    Router,
12};
13use llm_config_core::ConfigManager;
14use std::net::SocketAddr;
15use std::sync::Arc;
16use tower_http::cors::{Any, CorsLayer};
17use tower_http::trace::TraceLayer;
18
19/// Server configuration
20#[derive(Debug, Clone)]
21pub struct ServerConfig {
22    pub host: String,
23    pub port: u16,
24    pub enable_cors: bool,
25    pub enable_security: bool,
26}
27
28impl Default for ServerConfig {
29    fn default() -> Self {
30        Self {
31            host: "127.0.0.1".to_string(),
32            port: 8080,
33            enable_cors: true,
34            enable_security: true,
35        }
36    }
37}
38
39/// Create and configure the Axum router
40pub fn create_router(manager: Arc<ConfigManager>, security_state: SecurityState) -> Router {
41    let api_state = ApiState { manager };
42
43    // API v1 routes with security middleware
44    let api_routes = Router::new()
45        // Config operations
46        .route("/configs/:namespace/:key", get(get_config))
47        .route("/configs/:namespace/:key", post(set_config))
48        .route("/configs/:namespace/:key", delete(delete_config))
49        .route("/configs/:namespace", get(list_configs))
50        // Version history and rollback
51        .route("/configs/:namespace/:key/history", get(get_history))
52        .route(
53            "/configs/:namespace/:key/rollback/:version",
54            post(rollback_config),
55        )
56        .layer(middleware::from_fn_with_state(
57            security_state.clone(),
58            comprehensive_security_middleware,
59        ))
60        .with_state(api_state);
61
62    // Main router with health check (no security on health endpoint)
63    Router::new()
64        .route("/health", get(health_check))
65        .nest("/api/v1", api_routes)
66}
67
68/// Start the HTTP server
69pub async fn serve(
70    manager: Arc<ConfigManager>,
71    config: ServerConfig,
72) -> anyhow::Result<()> {
73    // Create security state
74    let security_state = if config.enable_security {
75        SecurityState::new()
76    } else {
77        SecurityState::new() // Always create but can be configured differently
78    };
79
80    let app = create_router(manager, security_state);
81
82    // Add middleware layers
83    let app = app
84        .layer(TraceLayer::new_for_http())
85        .layer(if config.enable_cors {
86            CorsLayer::new()
87                .allow_origin(Any)
88                .allow_methods(Any)
89                .allow_headers(Any)
90        } else {
91            CorsLayer::permissive()
92        });
93
94    // Bind to address
95    let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?;
96    tracing::info!(
97        "Starting LLM Config API server on {} (security: {})",
98        addr,
99        config.enable_security
100    );
101
102    // Create listener
103    let listener = tokio::net::TcpListener::bind(addr).await?;
104
105    // Serve with graceful shutdown
106    axum::serve(
107        listener,
108        app.into_make_service_with_connect_info::<SocketAddr>(),
109    )
110    .with_graceful_shutdown(shutdown_signal())
111    .await?;
112
113    Ok(())
114}
115
116/// Graceful shutdown handler
117async fn shutdown_signal() {
118    let ctrl_c = async {
119        tokio::signal::ctrl_c()
120            .await
121            .expect("Failed to install Ctrl+C handler");
122    };
123
124    #[cfg(unix)]
125    let terminate = async {
126        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
127            .expect("Failed to install signal handler")
128            .recv()
129            .await;
130    };
131
132    #[cfg(not(unix))]
133    let terminate = std::future::pending::<()>();
134
135    tokio::select! {
136        _ = ctrl_c => {
137            tracing::info!("Received Ctrl+C, shutting down gracefully");
138        },
139        _ = terminate => {
140            tracing::info!("Received SIGTERM, shutting down gracefully");
141        },
142    }
143}
144
145// Integration tests will be added in a separate test file
146// For now, the API can be tested manually using curl or similar tools