use aws_sdk_s3::operation::list_objects_v2::ListObjectsV2Output; use axum::{ Router, body::Body, extract::{Path, Query, State}, http::StatusCode, response::{IntoResponse, Json, Response}, routing::{delete, get, post, put}, }; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, sync::Arc}; use tokio::sync::Mutex; use tower::ServiceBuilder; use tower_http::{ cors::{Any, CorsLayer}, trace::{DefaultMakeSpan, TraceLayer}, }; use tracing::{debug, error, info}; use crate::{ discovery::ContainerDiscovery, log_info, logging::{LogDatesResponse, LogFilters, LogManager, LogResponse}, storage::Storage, types::{ApiResponse, HealthResponse, ManagedContainer, StatsResponse}, updater::UpdateManager, }; pub struct ApiServer { port: u16, update_manager: Arc>, discovery: Arc, storage: Arc, log_manager: Arc, s3_client: aws_sdk_s3::Client, garage_endpoint: String, } #[derive(Clone)] pub struct AppState { pub update_manager: Arc>, pub discovery: Arc, pub storage: Arc, pub log_manager: Arc, // garage pub s3_client: aws_sdk_s3::Client, pub garage_endpoint: String, } impl ApiServer { pub fn new( port: u16, update_manager: Arc>, discovery: Arc, storage: Arc, log_manager: Arc, s3_client: aws_sdk_s3::Client, garage_endpoint: String, ) -> Self { Self { port, update_manager, discovery, storage, log_manager, s3_client, garage_endpoint, } } pub async fn run(self) -> Result<(), Box> { let state = AppState { update_manager: self.update_manager, discovery: self.discovery, storage: self.storage, log_manager: self.log_manager.clone(), s3_client: self.s3_client, garage_endpoint: self.garage_endpoint, }; let app = Router::new() // Health and info endpoints .route("/health", get(health_check)) .route("/", get(root)) // Container management endpoints .route("/api/containers", get(list_containers)) .route("/api/containers/{id}", get(get_container)) .route("/api/containers/{id}/update", post(trigger_update)) .route("/api/containers/{id}/force-update", post(force_update)) .route("/api/containers/{id}/rollback", post(force_rollback)) // Discovery endpoints .route("/api/discovery/scan", post(force_discovery)) .route("/api/discovery/containers", get(get_discovered_containers)) // System endpoints .route("/api/status", get(system_status)) .route("/api/stats", get(get_stats)) .route("/api/logs", get(get_logs)) .route("/api/logs/dates", get(get_log_dates)) // Bulk operations .route("/api/bulk/update-check", post(bulk_update_check)) // TODO: communicate with garage s3 // .route("/api/s3/{bucket}", get(handler)) // .route("/api/s3/{bucket}", put(handler)) // .route("/api/s3/{bucket}", delete(handler)) // .route("/api/s3/{bucket}/{key}", get(handler)) // .route("/api/s3/{bucket}/{key}", put(handler)) // .route("/api/s3/{bucket}/{key}", delete(handler)) // TODO: Online installations .with_state(state) .layer( ServiceBuilder::new() .layer( TraceLayer::new_for_http() .make_span_with(DefaultMakeSpan::new().include_headers(true)), ) .layer( CorsLayer::new() .allow_origin(Any) .allow_headers(Any) .allow_methods(Any), ), ); let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", self.port)).await?; info!("🌐 API server listening on http://0.0.0.0:{}", self.port); log_info!( self.log_manager.clone(), "init", format!("🌐 API server listening on http://0.0.0.0:{}", self.port) ); axum::serve(listener, app).await?; Ok(()) } } // Handler functions async fn root() -> Json { Json(serde_json::json!({ "service": "Docker Update Manager", "version": "1.0.0", "description": "Automatic Docker container update management with service discovery", "endpoints": { "health": "/health", "status": "/api/status", "containers": "/api/containers", "discovery": "/api/discovery/scan", "stats": "/api/stats", "bulk_update": "/api/bulk/update-check" } })) } async fn health_check(State(state): State) -> Json> { log_info!(state.log_manager, "/health", "get health check..."); // let containers = match state.update_manager.lock().await.get_all_containers().await { // Ok(containers) => containers, // Err(_) => vec![], // }; let containers = { let mgr = state.update_manager.lock().await; let _containers = mgr.get_all_containers().await; drop(mgr); match _containers { Ok(containers) => containers, Err(_) => vec![], } }; debug!("containers: {:?}", containers); let active_updates = { let mgr = state.update_manager.lock().await; let _active_updates = mgr.get_active_updates_count().await; drop(mgr); _active_updates }; let update_queue_size = { let mgr = state.update_manager.lock().await; let _update_queue_size = mgr.get_update_queue_size().await; drop(mgr); _update_queue_size }; let health = HealthResponse { status: "healthy".to_string(), version: "1.0.0".to_string(), uptime_seconds: 0, // TODO: Track actual uptime managed_containers: containers.len(), active_updates, update_queue_size, }; Json(ApiResponse::success(health)) } async fn system_status(State(state): State) -> Json> { // let containers = state // .update_manager // .lock() // .await // .get_all_containers() // .await // .unwrap_or_default(); let containers = { let mgr = state.update_manager.lock().await; let _containers = mgr.get_all_containers().await.unwrap_or_default(); drop(mgr); _containers }; let discovered = state .discovery .get_discovered_containers() .await .unwrap_or_default(); let active_updates = { let mgr = state.update_manager.lock().await; let _active_updates = mgr.get_active_updates_count().await; drop(mgr); _active_updates }; let update_queue_size = { let mgr = state.update_manager.lock().await; let _update_queue_size = mgr.get_update_queue_size().await; drop(mgr); _update_queue_size }; let status = SystemStatusResponse { service: "Silserv".to_string(), version: "0.1.1".to_string(), managed_containers: containers.len(), discovered_containers: discovered.len(), active_updates, update_queue_size, containers: containers .into_iter() .map(|c| ContainerSummary { id: c.id, name: c.name, image_name: c.image_name, current_version: c.current_version, status: c.status, last_seen: c.last_seen, update_available: false, // TODO: Check if update is available }) .collect(), }; Json(ApiResponse::success(status)) } async fn list_containers( State(state): State, Query(params): Query, ) -> Result>>, ApiError> { let containers = { let mgr = state.update_manager.lock().await; let _containers = mgr.get_all_containers().await.unwrap_or_default(); drop(mgr); _containers }; let filtered_containers = if let Some(status_filter) = params.status { containers .into_iter() .filter(|c| format!("{:?}", c.status).to_lowercase() == status_filter.to_lowercase()) .collect() } else { containers }; Ok(Json(ApiResponse::success(filtered_containers))) } async fn get_container( Path(id): Path, State(state): State, ) -> Result>, ApiError> { let container = state.update_manager.lock().await.get_container(&id).await?; Ok(Json(ApiResponse::success(container))) } async fn trigger_update( Path(id): Path, State(state): State, ) -> Result>, ApiError> { // state // .update_manager // .lock() // .await // .check_container_for_updates(id.clone()) // .await?; info!("request update ..."); { UpdateManager::check_container_for_updates(state.update_manager.clone(), id.clone()) .await?; } let response = UpdateTriggeredResponse { container_id: id.clone(), message: "Update check triggered".to_string(), }; Ok(Json(ApiResponse::success(response))) } async fn force_update( Path(id): Path, State(state): State, Json(payload): Json, ) -> Result>, ApiError> { // state // .update_manager // .lock() // .await // .force_update(id.clone(), payload.target_version) // .await?; { // let mgr = state.update_manager.lock().await; UpdateManager::force_update( state.update_manager.clone(), id.clone(), payload.target_version, ) .await?; } let response = UpdateTriggeredResponse { container_id: id.clone(), message: "Forced update queued".to_string(), }; Ok(Json(ApiResponse::success(response))) } async fn force_discovery( State(state): State, ) -> Result>, ApiError> { state.discovery.force_discovery().await?; let response = DiscoveryResponse { message: "Container discovery scan completed".to_string(), }; Ok(Json(ApiResponse::success(response))) } async fn force_rollback( Path(id): Path, State(state): State, ) -> Result>, ApiError> { // find images with format of -backup- { let mgr = state.update_manager.lock().await; let container = mgr.get_container(&id).await; match container { Ok(mut c) => { mgr.attempt_rollback(&mut c).await?; } Err(err) => { error!("Failed to get container: {}", err); return Ok(Json(ApiResponse::error(err.to_string()))); } } } let success = RollbackResponse { message: "Rollback completed successfully".to_string(), }; Ok(Json(ApiResponse::success(success))) } async fn get_discovered_containers( State(state): State, ) -> Result>>, ApiError> { let containers = state.discovery.get_discovered_containers().await?; Ok(Json(ApiResponse::success(containers))) } async fn get_stats( State(state): State, ) -> Result>, ApiError> { let mut error_flag = false; let stats = { let mgr = state.update_manager.lock().await; let _stats = mgr.get_update_stats().await; drop(mgr); match _stats { Ok(stats) => stats, Err(err) => { error!("Failed to get update stats: {}", err); error_flag = true; StatsResponse::default() } } }; Ok(Json(ApiResponse::success(stats))) } // Logs endpoint handler async fn get_logs( State(state): State, Query(params): Query>, ) -> Result>, ApiError> { let filters = LogFilters { date: params.get("date").cloned(), level: params.get("level").cloned(), component: params.get("component").cloned(), container: params.get("container").cloned(), search: params.get("search").cloned(), page: params .get("page") .and_then(|p| p.parse().ok()) .unwrap_or(1) .max(1), limit: params .get("limit") .and_then(|l| l.parse().ok()) .unwrap_or(100) .min(1000) // Max 1000 logs per request .max(1), }; let logs = state.log_manager.get_logs(filters).await?; Ok(Json(ApiResponse::success(logs))) } async fn get_log_dates( State(state): State, ) -> Result>, ApiError> { let available_dates = state.log_manager.get_available_dates().await?; let current_date = chrono::Utc::now().format("%Y-%m-%d").to_string(); let response = LogDatesResponse { available_dates, current_date, }; Ok(Json(ApiResponse::success(response))) } async fn bulk_update_check( State(state): State, Json(payload): Json, ) -> Result>, ApiError> { let containers = if payload.container_ids.is_empty() { state .update_manager .lock() .await .get_all_containers() .await? } else { let mut selected = Vec::new(); for id in &payload.container_ids { if let Ok(container) = state.update_manager.lock().await.get_container(id).await { selected.push(container); } } selected }; let mut results = Vec::new(); for container in containers { let status = { let update_manager = state.update_manager.clone(); let container_id = container.container_id.clone(); async move { UpdateManager::check_container_for_updates(update_manager, container_id).await } }; match status.await { Ok(_) => { results.push(BulkUpdateResult { container_id: container.container_id, container_name: container.name, status: "triggered".to_string(), error: None, }); } Err(e) => { results.push(BulkUpdateResult { container_id: container.container_id, container_name: container.name, status: "failed".to_string(), error: Some(e.to_string()), }); } } } let response = BulkUpdateResponse { results }; Ok(Json(ApiResponse::success(response))) } // get object list async fn list_objects( State(state): State, Path(bucket): Path, Query(params): Query>, ) -> Result>, ApiError> { let mut list_request = state.s3_client.list_objects_v2().bucket(&bucket); if let Some(prefix) = params.get("prefix") { list_request = list_request.prefix(prefix); } let response: ListObjectsV2Output = list_request.send().await?; if response.contents.is_some() { let mapped = response .contents() .iter() .map(|obj| { let mut hashmap = HashMap::new(); hashmap.insert("Key".to_string(), obj.key().unwrap().to_string()); hashmap.insert("Size".to_string(), obj.size().unwrap().to_string()); hashmap.insert( "LastModified".to_string(), obj.last_modified().unwrap().to_string(), ); hashmap.insert("ETag".to_string(), obj.e_tag().unwrap().to_string()); hashmap.insert( "StorageClass".to_string(), obj.storage_class().unwrap().to_string(), ); hashmap.insert( "Owner".to_string(), obj.owner().unwrap().id().unwrap().to_string(), ); hashmap }) .collect::>>(); // let mut response = Response::builder() // .status(StatusCode::OK) // .body(Body::new(mapped)) // .unwrap(); Ok(Json(ApiResponse::success(serde_json::json!(mapped)))) } else { Ok(Json(ApiResponse::success(serde_json::json!({})))) } } // Request/Response types #[derive(Debug, Deserialize)] struct ListContainersQuery { status: Option, } #[derive(Debug, Deserialize)] struct LogsQuery { page: Option, limit: Option, level: Option, container: Option, } #[derive(Debug, Deserialize)] struct ForceUpdateRequest { target_version: Option, } #[derive(Debug, Deserialize)] struct BulkUpdateRequest { container_ids: Vec, } #[derive(Debug, Serialize)] struct SystemStatusResponse { service: String, version: String, managed_containers: usize, discovered_containers: usize, active_updates: usize, update_queue_size: usize, containers: Vec, } #[derive(Debug, Serialize)] struct ContainerSummary { id: String, name: String, image_name: String, current_version: String, status: crate::types::ContainerStatus, last_seen: chrono::DateTime, update_available: bool, } #[derive(Debug, Serialize)] struct UpdateTriggeredResponse { container_id: String, message: String, } #[derive(Debug, Serialize)] struct DiscoveryResponse { message: String, } #[derive(Debug, Serialize)] struct LogsResponse { logs: Vec, total: u32, page: u32, limit: u32, } #[derive(Debug, Serialize)] struct LogEntry { timestamp: String, level: String, message: String, container: Option, } #[derive(Debug, Serialize)] struct BulkUpdateResponse { results: Vec, } #[derive(Debug, Serialize)] struct BulkUpdateResult { container_id: String, container_name: String, status: String, error: Option, } #[derive(Debug, Serialize)] struct RollbackResponse { message: String, } // Error handling #[derive(Debug)] struct ApiError(anyhow::Error); impl IntoResponse for ApiError { fn into_response(self) -> Response { error!("API Error: {}", self.0); let error_response = ApiResponse::<()>::error(self.0.to_string()); (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)).into_response() } } impl From for ApiError where E: Into, { fn from(err: E) -> Self { Self(err.into()) } }