添加qiming-rcoder模块

This commit is contained in:
Codex
2026-06-01 13:54:52 +08:00
parent 8092c4b1f8
commit 4b1a580132
539 changed files with 151650 additions and 0 deletions

View File

@@ -0,0 +1,767 @@
use arc_swap::ArcSwap;
use clap::Parser;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tracing::{error, info, warn};
use hyper::server::conn::http1;
use hyper_util::rt::TokioIo;
use hyper_util::service::TowerToHyperService;
// 🆕 使用共享的遥测模块
use rcoder_telemetry::{TelemetryConfig, TelemetryGuard};
mod config;
mod config_watcher;
mod handler;
mod cleanup_task;
mod middleware;
mod router;
mod service;
mod utils;
use rcoder::*;
use config::{CliArgs, load_config_with_args};
use rcoder_proxy::{PingoraServerManager, ProxyConfig};
use router::AppState;
use service::{
ContainerStatusCheckerConfig, ContainerSyncConfig, VncSyncConfig,
start_container_status_checker, start_container_sync_task, start_vnc_sync_task,
};
// 导入统一的容器停止模块
use docker_manager::container_stop;
use docker_manager::runtime_selection::RuntimeType;
// 路由创建函数已移动到 handler 模块
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// ✅ 初始化 Rustls CryptoProvider必须在最前面在任何可能使用 TLS 的代码之前)
rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install rustls crypto provider");
// 解析命令行参数(移到最前面,以便尽早加载配置)
let cli_args = CliArgs::parse();
// 加载配置(包含命令行参数)
let config = load_config_with_args(cli_args)?;
// 🆕 Initializing telemetry system使用 rcoder-telemetry包含控制台 + 文件日志)
// 使用配置文件中的日志保留天数,与容器日志清理保持一致
let file_log_config = rcoder_telemetry::FileLogConfig::new("logs", "rcoder")
.with_max_files(config.cleanup_config.log_cleanup.log_retention_days as usize);
let telemetry_config =
TelemetryConfig::from_env("rcoder").with_file_log_config(file_log_config);
let telemetry: TelemetryGuard = rcoder_telemetry::init(telemetry_config).await?;
let telemetry = Arc::new(telemetry);
info!("Starting rcoder - AI-powered development platform");
info!(
"📋 Log config: keeping log files for {} days",
config.cleanup_config.log_cleanup.log_retention_days
);
// 创建项目工作目录
tokio::fs::create_dir_all(&config.projects_dir).await?;
info!("Projects directory: {:?}", config.projects_dir);
// 🔄 初始化宿主机路径解析器(自动检测模式)
// K8s 模式下不需要 Docker socket跳过路径解析器初始化
info!("starting to detect mount path...");
let runtime_type = docker_manager::runtime_selection::RuntimeType::from_env();
if runtime_type == docker_manager::runtime_selection::RuntimeType::Kubernetes {
info!("[K8S] Kubernetes runtime mode, skipping Docker socket path resolver");
} else {
let docker_socket_path = std::env::var("DOCKER_SOCKET_PATH").unwrap_or_else(|_| {
info!("DOCKER_SOCKET_PATH not set, using default: /var/run/docker.sock");
"/var/run/docker.sock".to_string()
});
info!("Docker socket: {}", docker_socket_path);
let _path_resolver =
match utils::HostPathResolver::new_with_docker_socket(Some(docker_socket_path.clone()))
.await
{
Ok(resolver) => {
info!("path resolver initialized successfully");
info!(
" Container workspace: {:?}",
resolver.container_workspace_base()
);
info!(
"work directory: {:?}",
resolver.host_workspace_base()
);
Some(resolver)
}
Err(e) => {
error!("path resolver initialization failed: {}", e);
error!("please check config:");
error!("1. Docker socket path: {}", docker_socket_path);
error!("2. Docker socket already mounted in container");
error!("3. container has Docker API access");
error!("4. project work directory mounted");
// 显示详细的错误信息和解决建议
show_docker_configuration_help(&docker_socket_path);
// 返回错误,停止启动
return Err(anyhow::anyhow!("Container self-check failed, unable to initialize path resolver"));
}
};
}
// 🧹 启动时清理上次可能遗留的容器
// 先使用正确配置初始化全局 DockerManager
info!("initialize Docker Manager (with config)...");
// 从应用配置创建 DockerManagerConfig
let docker_manager_config = if let Some(docker_config) = &config.docker_config {
info!("using Docker config, merging config");
let mut default_config = docker_manager::DockerManagerConfig::default();
// 合并应用配置中的多镜像配置
let app_multi_config = docker_config.get_multi_image_config();
default_config.multi_image_config = app_multi_config;
// 应用其他配置
default_config.auto_cleanup = docker_config
.auto_cleanup
.unwrap_or(default_config.auto_cleanup);
if let Some(ttl) = docker_config.container_ttl_seconds {
default_config.container_ttl_seconds = Some(ttl);
}
// 应用网络基础名称配置
info!(
"🔍 [DEBUG] docker_config.network_base_name = {:?}",
docker_config.network_base_name
);
if let Some(ref network_base_name) = docker_config.network_base_name {
info!("using config: {}", network_base_name);
default_config.network_base_name = network_base_name.clone();
} else {
info!(
"⚠️ No network_base_name in config, using default: {}",
default_config.network_base_name
);
}
// 🔧 应用超时配置
if let Some(timeout) = docker_config.api_timeout_seconds {
default_config.api_timeout_seconds = timeout;
info!("using config: API timeout: {} seconds", timeout);
}
if let Some(timeout) = docker_config.api_timeout_quick_seconds {
default_config.api_timeout_quick_seconds = timeout;
info!("using config: timeout: {} seconds", timeout);
}
// 🔧 应用缓存 TTL 配置
if let Some(ttl) = docker_config.cache_status_ttl_seconds {
default_config.cache_status_ttl_seconds = ttl;
info!(
"using config: status cache TTL: {} seconds",
ttl
);
}
if let Some(ttl) = docker_config.cache_network_ttl_seconds {
default_config.cache_network_ttl_seconds = ttl;
info!("using config: network cache TTL: {} seconds", ttl);
}
if let Some(capacity) = docker_config.cache_max_capacity {
default_config.cache_max_capacity = capacity as u64;
info!("using config: cache max capacity: {}", capacity);
}
default_config
} else {
info!("⚠️ no Docker config, using default config");
docker_manager::DockerManagerConfig::default()
};
// 使用自定义配置初始化全局 DockerManager
if let Err(e) =
docker_manager::global::init_global_docker_manager_with_config(docker_manager_config).await
{
error!("Docker Manager initializefailed: {}", e);
return Err(anyhow::anyhow!("Docker Manager initialization failed: {}", e));
}
info!("checking cleanup for container (enabled)...");
if config.cleanup_config.enabled {
match docker_manager::runtime::RuntimeManager::runtime_type() {
RuntimeType::Docker => {
let docker_manager = match docker_manager::global::get_global_docker_manager().await {
Ok(dm) => {
info!("Docker Manager initialized successfully (with config)");
dm
}
Err(e) => {
error!("get Docker Manager failed: {}", e);
return Err(anyhow::anyhow!("Failed to get Docker Manager: {}", e));
}
};
let multi_image_config = if let Some(docker_config) = &config.docker_config {
docker_config.get_multi_image_config()
} else {
shared_types::create_default_multi_image_config()
};
match container_stop::startup_cleanup_all_enabled_services(
&docker_manager,
&multi_image_config,
)
.await
{
Ok(result) => {
let enabled_services = shared_types::get_enabled_service_types(&multi_image_config);
if result.successfully_removed > 0 {
info!(
"✅ Startup cleanup completed, removed {} leftover containers (covering {} service types)",
result.successfully_removed,
enabled_services.len()
);
} else {
info!("no containers to cleanup");
}
if result.failed_removals > 0 {
warn!(
"container cleanup failed: failed count={}",
result.failed_removals
);
for failure in &result.failed_removals_details {
warn!(
" - Container {} ({}): {}",
failure.container_id, failure.container_name, failure.error_message
);
}
}
}
Err(e) => {
warn!("container cleanup failed: {}, cleanup skipped", e);
}
}
}
RuntimeType::Kubernetes => {
match docker_manager::runtime::RuntimeManager::get().await {
Ok(runtime) => {
if let Err(e) = runtime.cleanup_all().await {
warn!("k8s startup cleanup failed: {}", e);
} else {
info!("k8s startup cleanup completed");
}
}
Err(e) => warn!("failed to get runtime for k8s startup cleanup: {}", e),
}
}
}
} else {
info!("Container cleanup task already started (cleanup_config.enabled=false)");
}
// 从配置文件读取清理配置
let cleanup_config = cleanup_task::CleanupConfig {
idle_timeout: Duration::from_secs(config.cleanup_config.idle_timeout_seconds),
cleanup_interval: Duration::from_secs(config.cleanup_config.cleanup_interval_seconds),
docker_stop_timeout: Duration::from_secs(config.cleanup_config.docker_stop_timeout_seconds),
container_protection_duration: Duration::from_secs(
config.cleanup_config.container_protection_seconds,
),
active_window: Duration::from_secs(5 * 60),
log_dir: config.cleanup_config.log_cleanup.log_dir.clone(),
log_retention_duration: Duration::from_secs(
config.cleanup_config.log_cleanup.log_retention_days * 24 * 60 * 60,
),
};
info!(
"🧹 Cleanup config: idle_timeout={}s, cleanup_interval={}s, docker_stop_timeout={}s, container_protection={}s, log_dir={}, log_retention={}days",
config.cleanup_config.idle_timeout_seconds,
config.cleanup_config.cleanup_interval_seconds,
config.cleanup_config.docker_stop_timeout_seconds,
config.cleanup_config.container_protection_seconds,
config.cleanup_config.log_cleanup.log_dir,
config.cleanup_config.log_cleanup.log_retention_days
);
// proxy_manager 不需要直接访问 app_state通过参数传递即可
// 🆕 创建 API Key 配置的共享引用(用于热更新)
// 使用 ArcSwap 实现无锁读取,提升并发性能
let api_key_config = Arc::new(ArcSwap::from_pointee(config.api_key_auth.clone()));
// 启动代理服务(如果启用)
let (proxy_handle, pingora_service_opt, _pingora_shutdown_tx) = if let Some(proxy_config) =
&config.proxy_config
{
info!(
"Starting Pingora reverse proxy service, listening on port: {}",
proxy_config.listen_port
);
info!(
"Proxy route format: /proxy/{{port}}{{/path}} - e.g.: /proxy/{}/health",
config.port
);
// 添加调试日志
info!("🔧 [Pingora] startinginitialize Pingora config...");
info!("🔧 [Pingora] listenport: {}", proxy_config.listen_port);
info!(
"🔧 [Pingora] Default backend port: {}",
proxy_config.default_backend_port
);
info!("🔧 [Pingora] backend host: {}", proxy_config.backend_host);
let pingora_config = ProxyConfig {
listen_port: proxy_config.listen_port,
default_backend_port: proxy_config.default_backend_port,
backend_host: proxy_config.backend_host.clone(),
port_param: proxy_config.port_param.clone(),
config_file: None,
verbose: false,
};
info!("[Pingora] Pingora configcreatedsucceeded");
// 创建 Pingora 服务器管理器,并提取服务引用用于指标读取
info!("🔧 [Pingora] created PingoraServerManager...");
let mut server_manager = PingoraServerManager::new(pingora_config)
.with_api_key_config(Arc::clone(&api_key_config)); // 🆕 传递 API Key 配置
let pingora_service = server_manager.service();
info!("[Pingora] PingoraServerManager createdsucceeded");
info!("[Pingora] API Key config already loaded (no updates)");
// 启动健康检查循环(按配置)
if proxy_config.health_check.enabled {
let hc = &proxy_config.health_check;
info!(
"🔧 [Pingora] Starting health check loop: interval={}s, timeout={}s",
hc.interval_seconds, hc.timeout_seconds
);
pingora_service
.start_health_check_loop(hc.interval_seconds, (hc.timeout_seconds * 1000) as u64);
info!("[Pingora] health check already started");
}
// 启动 Pingora 服务器(如果启动失败,直接退出程序)
info!("[Pingora] starting Pingora server...");
let (pingora_shutdown_tx, pingora_shutdown_rx) = tokio::sync::oneshot::channel();
let handle = tokio::spawn(async move {
info!("📍 [Pingora] calling server_manager.start()...");
if let Err(e) = server_manager.start(pingora_shutdown_rx).await {
error!(
"[Pingora] Pingora proxy start failed, error: {:?}",
e
);
std::process::exit(1);
}
info!("[Pingora] server started");
});
info!("[Pingora] already started");
(
Some(handle),
Some(pingora_service),
Some(pingora_shutdown_tx),
)
} else {
info!("⚠️ [Pingora] proxy_config notconfig, skip Pingora started");
(None, None, None)
};
// 设置 Ctrl+C 信号处理
let shutdown_tx = setup_signal_handlers();
let shutdown_rx = shutdown_tx.subscribe();
// 🆕 启动配置文件监控(支持 API Key 热更新)
// 保持 watcher 的所有权,防止被提前 drop
let config_path = std::path::PathBuf::from(crate::config::CONFIG_FILE);
let _config_watcher =
match crate::config_watcher::ConfigWatcher::new(config_path, Arc::clone(&api_key_config)) {
Ok(watcher) => {
info!("Config file watcher already started, API Key updated");
Some(watcher)
}
Err(e) => {
warn!(
"config file watcher start failed: {}, API Key updated",
e
);
None
}
};
// 获取容器前缀(从配置读取,用于 pod_count 和 pod_list
let docker_config = config.docker_config.as_ref()
.ok_or_else(|| anyhow::anyhow!("Docker config is required for container prefix"))?;
let multi_config = docker_config.get_multi_image_config();
let selector = docker_manager::image_selector::ImageSelector::new(multi_config);
// 使用 block_in_place 在同步上下文中获取异步配置
let (container_prefix_rcoder, container_prefix_computer) = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
let rcoder_prefix = selector
.get_service_config(&shared_types::ServiceType::RCoder)
.await
.expect("Failed to get RCoder service config")
.container_prefix()
.to_string();
let computer_prefix = selector
.get_service_config(&shared_types::ServiceType::ComputerAgentRunner)
.await
.expect("Failed to get ComputerAgentRunner service config")
.container_prefix()
.to_string();
(rcoder_prefix, computer_prefix)
})
});
let state = Arc::new(AppState::new(
config.clone(),
pingora_service_opt,
api_key_config,
container_prefix_rcoder,
container_prefix_computer,
)?);
// 在主异步运行时中启动清理任务(如果启用)
let _cleanup_handle = if config.cleanup_config.enabled {
let cleanup_config_clone = cleanup_config.clone();
let state_for_cleanup = state.clone();
Some(
cleanup_task::start_cleanup_task(cleanup_config_clone, state_for_cleanup)
.await
.map_err(|e| anyhow::anyhow!("Failed to start cleanup task: {}", e))?,
)
} else {
info!("Container cleanup task already started (cleanup_config.enabled=false)");
None
};
// 启动容器状态检查任务(防止长时间任务的容器被误杀)
// 🆕 使用增强的配置,包含失败计数器和智能跳过机制
let status_checker_config = ContainerStatusCheckerConfig {
check_interval: Duration::from_secs(30), // 每 30 秒检查一次
query_timeout: Duration::from_secs(5), // 查询超时 5 秒
failure_threshold: 3, // 连续失败 3 次后跳过
skip_duration: Duration::from_secs(5 * 60), // 跳过 5 分钟
health_reset_interval: Duration::from_secs(30 * 60), // 30 分钟清理一次
};
let _status_checker_handle =
start_container_status_checker(status_checker_config, state.clone());
info!(
"Container status checker already started (interval: 30s, will skip Docker on failure)"
);
// 启动容器状态同步任务(定期检测被外部删除的容器)
let container_sync_config = ContainerSyncConfig {
sync_interval: Duration::from_secs(60), // 每 60 秒同步一次
};
let _container_sync_handle = start_container_sync_task(
container_sync_config,
state.grpc_pool.clone(),
);
info!(
"Container status sync already started (interval: 60s, detect container)"
);
// 🆕 启动 VNC 后端同步任务(定期从 Docker 同步容器 IP 到 Pingora
if let Some(ref pingora_service) = state.pingora_service {
let vnc_sync_config = VncSyncConfig {
sync_interval: Duration::from_secs(5), // 每 5 秒同步一次
};
let _vnc_sync_handle = start_vnc_sync_task(
pingora_service.clone(),
vnc_sync_config,
state.container_prefix_rcoder.clone(),
state.container_prefix_computer.clone(),
);
info!(
"VNC sync already started (interval: 5s, sync Docker container IP)"
);
}
// 创建路由(传入遥测 guard 用于 /metrics 端点)
let app = router::create_router(state.clone(), Some(telemetry.clone()));
// 启动 HTTP 服务器
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", config.port))
.await
.map_err(|e| anyhow::anyhow!("HTTP server failed to bind port {}: {}", config.port, e))?;
info!("Server starting on port {}", config.port);
info!("API endpoints:");
info!(" POST /chat - Send chat message to AI agent (legacy)");
info!(" GET /progress/:session_id - SSE progress stream for AI tasks (unified stream)");
info!(" GET /health - Health check");
info!(" NOTE: Plan data is delivered via the unified /progress/{{session_id}} SSE stream");
if let Some(proxy_config) = &config.proxy_config {
info!("Pingora proxy already started");
info!("📡 listenport: {}", proxy_config.listen_port);
info!("route: /proxy/{{port}}{{/path}} - example: /proxy/3000/api/users");
info!("format: query port parameter to proxy request");
info!("💡 example:");
info!(
" http://localhost:{}/proxy/{}/health → http://127.0.0.1:{}/health",
proxy_config.listen_port, config.port, config.port
);
info!(
" http://localhost:{}/proxy/{}/health → http://127.0.0.1:{}/health",
proxy_config.listen_port, config.port, config.port
);
info!(
" http://localhost:{}/proxy/9000/health → http://127.0.0.1:9000/health (dynamic discovery)",
proxy_config.listen_port
);
}
// 启动服务器,支持优雅关闭
// 使用自定义 Hyper 配置增加请求头大小限制(默认 8KB -> 128KB
info!("🔧 config HTTP max_buf_size = 128KB (to prevent HTTP 431 error)");
let app = app.into_make_service();
let mut shutdown_rx_clone = shutdown_tx.subscribe();
// 启动自定义 HTTP 服务器
let server_handle = tokio::spawn(async move {
loop {
tokio::select! {
// 等待关闭信号
_ = shutdown_rx_clone.recv() => {
info!("🛑 HTTP server closed");
break;
}
// 接受新连接
result = listener.accept() => {
match result {
Ok((stream, addr)) => {
let mut app_clone = app.clone();
tokio::spawn(async move {
// 配置 HTTP1增加 header 大小限制
let mut http_builder = http1::Builder::new();
http_builder
.max_buf_size(128 * 1024) // 128KB buffer默认约 8KB
.preserve_header_case(true)
.title_case_headers(false);
let io = TokioIo::new(stream);
// 使用 tower::Service 调用 MakeService
use tower::Service;
match std::future::poll_fn(|cx| {
Service::<std::net::SocketAddr>::poll_ready(&mut app_clone, cx)
}).await {
Ok(()) => {
match Service::<std::net::SocketAddr>::call(&mut app_clone, addr).await {
Ok(service) => {
let hyper_service = TowerToHyperService::new(service);
if let Err(e) = http_builder.serve_connection(io, hyper_service).await {
if !e.to_string().contains("connection closed")
&& !e.to_string().contains("early eof") {
tracing::debug!("HTTP connectionerror ({}): {}", addr, e);
}
}
}
Err(_) => {
// Infallible 类型,不会发生
}
}
}
Err(e) => {
tracing::error!("server error: {}", e);
}
}
});
}
Err(e) => {
error!("connection failed: {}", e);
}
}
}
}
}
});
// 等待服务器关闭
let _ = shutdown_signal(shutdown_rx).await;
server_handle.abort();
// 等待代理服务完成
if let Some(handle) = proxy_handle {
handle.await?;
}
Ok(())
}
/// 显示 Docker 配置帮助信息
fn show_docker_configuration_help(socket_path: &str) {
error!("📋 Docker config help:");
error!("");
error!("add to docker-compose.yml config:");
error!("");
error!("services:");
error!(" rcoder:");
error!(" environment:");
error!(" - DOCKER_SOCKET_PATH={}", socket_path);
error!(" volumes:");
error!(" - {}:/var/run/docker.sock:ro", socket_path);
error!(" - ./data/rcoder/project_workspace:/app/project_workspace");
error!("");
error!("🔧 Docker socket path:");
error!(" Linux: /var/run/docker.sock");
error!(" macOS + Docker Desktop: /var/run/docker.sock");
error!(" Rootless Docker: /run/user/$UID/docker.sock");
error!("");
error!("🛠️ troubleshooting:");
error!("1. check Docker: docker ps");
error!("2. check socket file exists: ls -l {}", socket_path);
error!("3. check docker group: groups $USER | grep docker");
error!(
" 4. Test Docker API: curl --unix-socket {} http://localhost/info",
socket_path
);
error!("");
error!("socket exists, rcoder container may not have access");
}
/// 设置信号处理器
fn setup_signal_handlers() -> tokio::sync::broadcast::Sender<()> {
let (shutdown_tx, _) = tokio::sync::broadcast::channel(1);
// 设置全局关闭标志
static SHUTDOWN_INITIATED: AtomicBool = AtomicBool::new(false);
// 注册 Ctrl+C 信号处理
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
let shutdown_tx_clone = shutdown_tx.clone();
tokio::spawn(async move {
// 注册信号处理器,如果失败则记录警告并优雅降级
let sigint_result = signal(SignalKind::interrupt());
let sigterm_result = signal(SignalKind::terminate());
match (sigint_result, sigterm_result) {
(Ok(mut sigint), Ok(mut sigterm)) => {
tokio::select! {
_ = sigint.recv() => {
if !SHUTDOWN_INITIATED.swap(true, Ordering::SeqCst) {
info!(" received SIGINT (Ctrl+C), starting graceful shutdown...");
let _ = shutdown_tx_clone.send(());
}
}
_ = sigterm.recv() => {
if !SHUTDOWN_INITIATED.swap(true, Ordering::SeqCst) {
info!(" received SIGTERM, starting graceful shutdown...");
let _ = shutdown_tx_clone.send(());
}
}
}
}
(Err(e), _) | (_, Err(e)) => {
warn!(" unix signal handler failed: {}, shutdown may not be graceful", e);
// 注册失败不影响程序运行,仍可通过其他方式关闭(如 tokio::signal::ctrl_c
}
}
});
}
#[cfg(not(unix))]
{
let shutdown_tx_clone = shutdown_tx.clone();
tokio::spawn(async move {
use tokio::signal;
if let Ok(()) = signal::ctrl_c().await {
if !SHUTDOWN_INITIATED.swap(true, Ordering::SeqCst) {
info!(" received Ctrl+C, starting graceful shutdown...");
let _ = shutdown_tx_clone.send(());
}
}
});
}
shutdown_tx
}
/// 优雅关闭信号处理
async fn shutdown_signal(mut shutdown_rx: tokio::sync::broadcast::Receiver<()>) {
// 等待关闭信号
let _ = shutdown_rx.recv().await;
info!("starting graceful shutdown...");
// 执行容器清理
if let Err(e) = cleanup_all_containers().await {
error!("container cleanup failed: {}", e);
} else {
info!("container cleanup completed");
}
info!("🛑 RCoder graceful shutdown completed");
}
/// 清理所有动态创建的容器
async fn cleanup_all_containers() -> anyhow::Result<()> {
info!("🧹 starting cleanup of dynamically created containers...");
match docker_manager::runtime::RuntimeManager::runtime_type() {
RuntimeType::Docker => {
let docker_manager = docker_manager::global::get_global_docker_manager()
.await
.map_err(|e| anyhow::anyhow!("Failed to get global DockerManager: {}", e))?;
let multi_image_config = shared_types::create_default_multi_image_config();
match container_stop::startup_cleanup_all_enabled_services(
&docker_manager,
&multi_image_config,
)
.await
{
Ok(result) => {
if result.successfully_removed > 0 {
info!(
"🧹 Cleaned up {} containers (all enabled services)",
result.successfully_removed
);
}
if result.failed_removals > 0 {
warn!(
"container cleanup failed: failed count={}",
result.failed_removals
);
}
}
Err(e) => {
warn!("container cleanup error: {}", e);
}
}
}
RuntimeType::Kubernetes => {
let runtime = docker_manager::runtime::RuntimeManager::get()
.await
.map_err(|e| anyhow::anyhow!("Failed to get runtime: {}", e))?;
runtime
.cleanup_all()
.await
.map_err(|e| anyhow::anyhow!("Failed to cleanup runtime resources: {}", e))?;
}
}
Ok(())
}