lmrc-cli 0.3.16

CLI tool for scaffolding LMRC Stack infrastructure projects
Documentation
//! Infrastructure API - Backend for infrastructure management
//!
//! A REST API service for managing infrastructure resources including:
//! - Projects
//! - Pipelines
//! - Deployments
//! - Kubernetes resources
//!
//! ## Architecture
//!
//! Hexagonal architecture (Ports & Adapters):
//! - **Inbound adapters**: HTTP handlers in `features/*/handlers.rs`
//! - **Core logic**: Business logic in feature modules
//! - **Outbound adapters**: Database access via SeaORM entities

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<()> {
    // Initialize tracing
    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();

    // Load environment variables from .env if present
    dotenvy::dotenv().ok();

    // Load configuration
    let config = config::Config::from_env()?;

    tracing::info!(
        "Starting Infrastructure API on {}:{}",
        config.server.host,
        config.server.port
    );

    // Connect to infrastructure database
    tracing::info!("Connecting to infrastructure database...");
    let db = Database::connect(&config.database.url).await?;
    tracing::info!("Infrastructure database connected successfully");

    // Create application state
    let state = AppState::new(config.clone(), db);

    // Build router
    let app = create_router(state);

    // Start server
    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(())
}

/// Create the application router
fn create_router(state: AppState) -> Router {
    Router::new()
        // Health check
        .route("/health", get(features::health::health_check))

        // Projects
        .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))

        // Pipelines
        .route("/api/projects/:project_id/pipelines", get(features::pipelines::list_pipelines))
        .route("/api/pipelines/:id", get(features::pipelines::get_pipeline))

        // Deployments
        .route("/api/projects/:project_id/deployments", get(features::deployments::list_deployments))
        .route("/api/deployments/:id", get(features::deployments::get_deployment))

        // Hetzner monitoring
        .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))

        // Kubernetes monitoring
        .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))

        // Vault secrets
        .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))

        // Authentication
        .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() {
        // Basic compilation test
        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,
            },
        };

        // Router creation should compile
        // Note: This will fail without a real database connection
        // In production tests, use a test database or mock
    }
}