photoncloud-monorepo/deployer/crates/deployer-server/src/lib.rs

136 lines
3.7 KiB
Rust

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<AppState>) -> 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);
}
}