675 lines
19 KiB
Rust
675 lines
19 KiB
Rust
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<Mutex<UpdateManager>>,
|
|
discovery: Arc<ContainerDiscovery>,
|
|
storage: Arc<Storage>,
|
|
log_manager: Arc<LogManager>,
|
|
s3_client: aws_sdk_s3::Client,
|
|
garage_endpoint: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct AppState {
|
|
pub update_manager: Arc<Mutex<UpdateManager>>,
|
|
pub discovery: Arc<ContainerDiscovery>,
|
|
pub storage: Arc<Storage>,
|
|
pub log_manager: Arc<LogManager>,
|
|
|
|
// garage
|
|
pub s3_client: aws_sdk_s3::Client,
|
|
pub garage_endpoint: String,
|
|
}
|
|
|
|
impl ApiServer {
|
|
pub fn new(
|
|
port: u16,
|
|
update_manager: Arc<Mutex<UpdateManager>>,
|
|
discovery: Arc<ContainerDiscovery>,
|
|
storage: Arc<Storage>,
|
|
log_manager: Arc<LogManager>,
|
|
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<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(),
|
|
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<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: "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<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 force_rollback(
|
|
Path(id): Path<String>,
|
|
State(state): State<AppState>,
|
|
) -> Result<Json<ApiResponse<RollbackResponse>>, ApiError> {
|
|
// find images with format of <name>-backup-<timestamp>
|
|
{
|
|
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<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)))
|
|
}
|
|
|
|
// get object list
|
|
async fn list_objects(
|
|
State(state): State<AppState>,
|
|
Path(bucket): Path<String>,
|
|
Query(params): Query<std::collections::HashMap<String, String>>,
|
|
) -> Result<Json<ApiResponse<serde_json::Value>>, 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::<Vec<HashMap<String, String>>>();
|
|
|
|
// 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<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>,
|
|
}
|
|
|
|
#[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<E> From<E> for ApiError
|
|
where
|
|
E: Into<anyhow::Error>,
|
|
{
|
|
fn from(err: E) -> Self {
|
|
Self(err.into())
|
|
}
|
|
}
|