pub mod admin; pub mod auth; pub mod bootstrap_assets; pub mod cloud_init; pub mod cluster; pub mod config; pub mod local_storage; pub mod phone_home; pub mod state; pub mod storage; pub mod tls; pub mod validation; use axum::{ routing::{get, post}, Router, }; use std::sync::Arc; use tracing::info; use crate::{config::Config, state::AppState}; /// Build the Axum router with all API routes pub fn build_router(state: Arc) -> Router { Router::new() // Health check .route("/health", get(health_check)) // Phone Home API (node registration) .route("/api/v1/phone-home", post(phone_home::phone_home)) .route( "/api/v1/cloud-init/:machine_id/meta-data", get(cloud_init::meta_data), ) .route( "/api/v1/cloud-init/:machine_id/user-data", get(cloud_init::user_data), ) .route( "/api/v1/bootstrap/flake-bundle", get(bootstrap_assets::flake_bundle), ) // Admin API (node management) .route("/api/v1/admin/nodes", post(admin::pre_register)) .route("/api/v1/admin/nodes", get(admin::list_nodes)) .with_state(state) } /// Health check endpoint async fn health_check() -> &'static str { "OK" } /// Run the Deployer server pub async fn run(config: Config) -> anyhow::Result<()> { let bind_addr = config.bind_addr; // Create application state let mut state = AppState::with_config(config); if state.config.allow_unauthenticated { tracing::warn!("Deployer running with allow_unauthenticated=true (unsafe)"); } else if state.config.bootstrap_token.is_none() { tracing::warn!("Deployer requires bootstrap_token but none is configured"); } if state.config.admin_token.is_none() { if state.config.allow_admin_fallback { tracing::warn!("admin_token not set; admin API will fall back to bootstrap_token"); } else { tracing::warn!( "admin_token not set; admin API disabled unless allow_admin_fallback=true" ); } } else if state.config.admin_token == state.config.bootstrap_token { tracing::warn!( "DEPLOYER_ADMIN_TOKEN matches bootstrap token; consider separating privileges" ); } if state.config.cluster_id.is_none() { tracing::warn!( "cluster_id not set; cluster node state won't be written to photoncloud/clusters" ); } // Initialize ChainFire storage if let Err(e) = state.init_storage().await { tracing::warn!(error = %e, "ChainFire storage initialization failed"); } if state.config.require_chainfire && !state.has_storage() { return Err(anyhow::anyhow!( "ChainFire storage is required but unavailable. Configure chainfire.endpoints or disable require_chainfire for dev mode." )); } let state = Arc::new(state); // Build router let app = build_router(state); // Create TCP listener let listener = tokio::net::TcpListener::bind(bind_addr).await?; info!("Deployer server listening on {}", bind_addr); // Run server axum::serve(listener, app).await?; Ok(()) } #[cfg(test)] mod tests { use super::*; use axum::http::StatusCode; use tower::ServiceExt; #[tokio::test] async fn test_health_check() { let state = Arc::new(AppState::new()); let app = build_router(state); let response = app .oneshot( axum::http::Request::builder() .uri("/health") .body(axum::body::Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } }