1use anyhow::Result;
7use axum::{routing::get, Router};
8use std::net::SocketAddr;
9use std::time::Duration;
10use tower_http::trace::TraceLayer;
11
12mod controllers;
13mod routes;
14mod storage;
15mod watch;
16
17#[cfg(test)]
18mod tests;
19
20pub use controllers::PlanResolver;
21pub use storage::Store;
22pub use watch::WatchBroadcaster;
23
24#[derive(Clone)]
26pub struct AppState {
27 pub store: Store,
28 pub broadcaster: WatchBroadcaster,
29}
30
31#[derive(Debug, Clone)]
33pub struct ServerConfig {
34 pub host: String,
35 pub port: u16,
36 pub db_path: String,
37 pub reconcile_interval_secs: u64,
38}
39
40impl Default for ServerConfig {
41 fn default() -> Self {
42 Self {
43 host: "127.0.0.1".to_string(),
44 port: 8080,
45 db_path: "planspec.db".to_string(),
46 reconcile_interval_secs: 5,
47 }
48 }
49}
50
51impl ServerConfig {
52 pub fn from_env() -> Self {
54 Self {
55 host: std::env::var("PLANSPEC_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
56 port: std::env::var("PORT")
57 .ok()
58 .and_then(|p| p.parse().ok())
59 .unwrap_or(8080),
60 db_path: std::env::var("PLANSPEC_DB").unwrap_or_else(|_| "planspec.db".to_string()),
61 reconcile_interval_secs: std::env::var("PLANSPEC_RECONCILE_INTERVAL")
62 .ok()
63 .and_then(|i| i.parse().ok())
64 .unwrap_or(5),
65 }
66 }
67}
68
69pub fn build_router(state: AppState) -> Router {
71 Router::new()
72 .route("/healthz", get(|| async { "ok" }))
73 .nest("/apis/planspec.io/v1alpha1", routes::api_routes())
74 .layer(TraceLayer::new_for_http())
75 .with_state(state)
76}
77
78pub async fn run_server(config: ServerConfig) -> Result<()> {
80 let store = Store::new(&config.db_path).await?;
82
83 let broadcaster = WatchBroadcaster::new();
85
86 let state = AppState {
87 store: store.clone(),
88 broadcaster: broadcaster.clone(),
89 };
90
91 let resolver = PlanResolver::new(store, broadcaster);
93 let interval = Duration::from_secs(config.reconcile_interval_secs);
94 tokio::spawn(async move {
95 loop {
96 if let Err(e) = resolver.reconcile_all().await {
97 tracing::warn!(error = %e, "Plan resolution reconcile failed");
98 }
99 tokio::time::sleep(interval).await;
100 }
101 });
102
103 let app = build_router(state);
105
106 let addr: SocketAddr = format!("{}:{}", config.host, config.port).parse()?;
108 tracing::info!("PlanSpec server listening on {}", addr);
109
110 let listener = tokio::net::TcpListener::bind(addr).await?;
111 axum::serve(listener, app).await?;
112
113 Ok(())
114}