initialize
This commit is contained in:
commit
f6e3fdd3c2
22 changed files with 7447 additions and 0 deletions
571
src/api.rs
Normal file
571
src/api.rs
Normal file
|
|
@ -0,0 +1,571 @@
|
|||
use axum::{
|
||||
Router,
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Json, Response},
|
||||
routing::{get, post},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::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<Mutex<UpdateManager>>,
|
||||
discovery: Arc<ContainerDiscovery>,
|
||||
storage: Arc<Storage>,
|
||||
log_manager: Arc<LogManager>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub update_manager: Arc<Mutex<UpdateManager>>,
|
||||
pub discovery: Arc<ContainerDiscovery>,
|
||||
pub storage: Arc<Storage>,
|
||||
pub log_manager: Arc<LogManager>,
|
||||
}
|
||||
|
||||
impl ApiServer {
|
||||
pub fn new(
|
||||
port: u16,
|
||||
update_manager: Arc<Mutex<UpdateManager>>,
|
||||
discovery: Arc<ContainerDiscovery>,
|
||||
storage: Arc<Storage>,
|
||||
log_manager: Arc<LogManager>,
|
||||
) -> Self {
|
||||
Self {
|
||||
port,
|
||||
update_manager,
|
||||
discovery,
|
||||
storage,
|
||||
log_manager,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let state = AppState {
|
||||
update_manager: self.update_manager,
|
||||
discovery: self.discovery,
|
||||
storage: self.storage,
|
||||
log_manager: self.log_manager.clone(),
|
||||
};
|
||||
|
||||
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))
|
||||
// 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))
|
||||
.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<serde_json::Value> {
|
||||
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<AppState>) -> Json<ApiResponse<HealthResponse>> {
|
||||
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<AppState>) -> Json<ApiResponse<SystemStatusResponse>> {
|
||||
// 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: "Docker Update Manager".to_string(),
|
||||
version: "1.0.0".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<AppState>,
|
||||
Query(params): Query<ListContainersQuery>,
|
||||
) -> Result<Json<ApiResponse<Vec<ManagedContainer>>>, 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<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<ApiResponse<ManagedContainer>>, ApiError> {
|
||||
let container = state.update_manager.lock().await.get_container(&id).await?;
|
||||
Ok(Json(ApiResponse::success(container)))
|
||||
}
|
||||
|
||||
async fn trigger_update(
|
||||
Path(id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<ApiResponse<UpdateTriggeredResponse>>, 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<String>,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<ForceUpdateRequest>,
|
||||
) -> Result<Json<ApiResponse<UpdateTriggeredResponse>>, 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<AppState>,
|
||||
) -> Result<Json<ApiResponse<DiscoveryResponse>>, ApiError> {
|
||||
state.discovery.force_discovery().await?;
|
||||
|
||||
let response = DiscoveryResponse {
|
||||
message: "Container discovery scan completed".to_string(),
|
||||
};
|
||||
|
||||
Ok(Json(ApiResponse::success(response)))
|
||||
}
|
||||
|
||||
async fn get_discovered_containers(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<ApiResponse<Vec<String>>>, ApiError> {
|
||||
let containers = state.discovery.get_discovered_containers().await?;
|
||||
Ok(Json(ApiResponse::success(containers)))
|
||||
}
|
||||
|
||||
async fn get_stats(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<ApiResponse<crate::types::StatsResponse>>, 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<AppState>,
|
||||
Query(params): Query<std::collections::HashMap<String, String>>,
|
||||
) -> Result<Json<ApiResponse<LogResponse>>, 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<AppState>,
|
||||
) -> Result<Json<ApiResponse<LogDatesResponse>>, 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<AppState>,
|
||||
Json(payload): Json<BulkUpdateRequest>,
|
||||
) -> Result<Json<ApiResponse<BulkUpdateResponse>>, 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)))
|
||||
}
|
||||
|
||||
// Request/Response types
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ListContainersQuery {
|
||||
status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LogsQuery {
|
||||
page: Option<u32>,
|
||||
limit: Option<u32>,
|
||||
level: Option<String>,
|
||||
container: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ForceUpdateRequest {
|
||||
target_version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct BulkUpdateRequest {
|
||||
container_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SystemStatusResponse {
|
||||
service: String,
|
||||
version: String,
|
||||
managed_containers: usize,
|
||||
discovered_containers: usize,
|
||||
active_updates: usize,
|
||||
update_queue_size: usize,
|
||||
containers: Vec<ContainerSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ContainerSummary {
|
||||
id: String,
|
||||
name: String,
|
||||
image_name: String,
|
||||
current_version: String,
|
||||
status: crate::types::ContainerStatus,
|
||||
last_seen: chrono::DateTime<chrono::Utc>,
|
||||
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<LogEntry>,
|
||||
total: u32,
|
||||
page: u32,
|
||||
limit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct LogEntry {
|
||||
timestamp: String,
|
||||
level: String,
|
||||
message: String,
|
||||
container: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct BulkUpdateResponse {
|
||||
results: Vec<BulkUpdateResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct BulkUpdateResult {
|
||||
container_id: String,
|
||||
container_name: String,
|
||||
status: String,
|
||||
error: Option<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<E> From<E> for ApiError
|
||||
where
|
||||
E: Into<anyhow::Error>,
|
||||
{
|
||||
fn from(err: E) -> Self {
|
||||
Self(err.into())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue