mod config;
mod error;
mod features;
mod state;
use axum::{
middleware,
routing::{delete, get, post, put},
Router,
};
use lmrc_http_common::middleware::cors_with_origins;
use sea_orm::Database;
use state::AppState;
use std::net::SocketAddr;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "infra_api=debug,tower_http=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
dotenvy::dotenv().ok();
let config = config::Config::from_env()?;
tracing::info!(
"Starting Infrastructure API on {}:{}",
config.server.host,
config.server.port
);
tracing::info!("Connecting to infrastructure database...");
let db = Database::connect(&config.database.url).await?;
tracing::info!("Infrastructure database connected successfully");
let state = AppState::new(config.clone(), db);
let app = create_router(state);
let addr = SocketAddr::from(([0, 0, 0, 0], config.server.port));
tracing::info!("Listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
fn create_router(state: AppState) -> Router {
Router::new()
.route("/health", get(features::health::health_check))
.route("/api/projects", get(features::projects::list_projects))
.route("/api/projects/:id", get(features::projects::get_project))
.route("/api/projects", post(features::projects::create_project))
.route("/api/projects/:id", put(features::projects::update_project))
.route("/api/projects/:id", delete(features::projects::delete_project))
.route("/api/projects/:project_id/pipelines", get(features::pipelines::list_pipelines))
.route("/api/pipelines/:id", get(features::pipelines::get_pipeline))
.route("/api/projects/:project_id/deployments", get(features::deployments::list_deployments))
.route("/api/deployments/:id", get(features::deployments::get_deployment))
.route("/api/hetzner/servers", get(features::hetzner::list_servers))
.route("/api/hetzner/networks", get(features::hetzner::list_networks))
.route("/api/hetzner/load-balancers", get(features::hetzner::list_load_balancers))
.route("/api/hetzner/firewalls", get(features::hetzner::list_firewalls))
.route("/api/kubernetes/clusters/:id/nodes", get(features::kubernetes::list_nodes))
.route("/api/kubernetes/clusters/:id/pods", get(features::kubernetes::list_pods))
.route("/api/kubernetes/clusters/:id/services", get(features::kubernetes::list_services))
.route("/api/kubernetes/clusters/:id/deployments", get(features::kubernetes::list_deployments))
.route("/api/vault/secrets/*path", get(features::vault::read_secret))
.route("/api/vault/secrets/*path", post(features::vault::write_secret))
.route("/api/vault/secrets/*path", delete(features::vault::delete_secret))
.route("/api/vault/auth/token", post(features::vault::create_token))
.route("/api/auth/me", get(features::auth::get_current_user))
.route("/api/auth/tokens", post(features::auth::create_api_token))
.route("/api/auth/tokens", get(features::auth::list_api_tokens))
.route("/api/auth/tokens/:id", delete(features::auth::revoke_api_token))
.layer(TraceLayer::new_for_http())
.layer(cors_with_origins(state.config.server.cors_origins.clone()))
.with_state(state)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_router_compiles() {
let config = config::Config {
server: config::ServerConfig {
host: "localhost".to_string(),
port: 8081,
cors_origins: vec!["http://localhost:3000".to_string()],
},
database: config::DatabaseConfig {
url: "postgres://test:test@localhost/infra_db".to_string(),
max_connections: 5,
},
};
}
}