Files
qiming/qiming-rcoder/crates/docker_manager/src/manager.rs
2026-06-01 13:54:52 +08:00

3449 lines
137 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::{
CleanupOptions, CleanupResult, ContainerFilter, ContainerQueryResult, ContainerQueryResultArc,
ContainerRemovalFailure, ContainerStatus, DockerContainerConfig, DockerContainerInfo,
DockerError, DockerManagerConfig, DockerResult,
};
use container_runtime_api::{ContainerRuntimeStatus, RemovedContainerInfo};
use crate::container_state_actor::{ContainerStateActor, ContainerStateHandle};
use anyhow::Result;
use bollard::query_parameters::{
CreateContainerOptions, CreateImageOptions, InspectContainerOptions, ListContainersOptions,
LogsOptions, RemoveContainerOptions, RestartContainerOptions, StartContainerOptions,
StopContainerOptions,
};
use bollard::{
API_DEFAULT_VERSION, Docker,
models::{
ContainerCreateBody, ContainerSummary, HostConfig, Mount, NetworkingConfig, PortBinding,
},
};
use chrono::{DateTime, Utc};
use moka::future::Cache;
use shared_types::{ContainerBasicInfo, ServiceType};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tracing::{debug, error, info, warn};
/// Docker API 缓存
///
/// 使用 Moka 缓存库实现高性能缓存,减少 Docker API 调用次数
/// 使用结构体包装,提高代码可读性和减少 clone 开销
pub struct DockerApiCache {
/// 容器状态缓存 (identifier -> Option<ContainerQueryResultArc>)
/// 支持 None 值缓存,用于缓存 404 响应
status_cache: Cache<String, Option<ContainerQueryResultArc>>,
/// 网络信息缓存 (container_id -> Option<Arc<HashMap<network_name, ip_address>>>)
/// 支持 None 值缓存
network_cache: Cache<String, Option<Arc<HashMap<String, String>>>>,
}
impl DockerApiCache {
/// 创建新的缓存实例
///
/// # 参数
/// * `status_ttl` - 状态缓存 TTL
/// * `network_ttl` - 网络缓存 TTL
/// * `max_capacity` - 缓存最大容量
pub fn new(status_ttl: u64, network_ttl: u64, max_capacity: u64) -> Self {
info!(
"Initializing Docker API cache: status_ttl={}s, network_ttl={}s, max_capacity={}",
status_ttl, network_ttl, max_capacity
);
Self {
status_cache: Cache::builder()
.max_capacity(max_capacity)
.time_to_live(Duration::from_secs(status_ttl))
.build(),
network_cache: Cache::builder()
.max_capacity(max_capacity)
.time_to_live(Duration::from_secs(network_ttl))
.build(),
}
}
/// 使用默认配置创建缓存实例
#[allow(dead_code)]
pub fn with_defaults() -> Self {
Self::new(10, 15, 10000)
}
/// 获取状态缓存
pub async fn get_status(&self, identifier: &str) -> Option<Option<ContainerQueryResultArc>> {
self.status_cache.get(identifier).await
}
/// 写入状态缓存(支持 None 值)
pub async fn insert_status(&self, identifier: String, value: Option<ContainerQueryResultArc>) {
self.status_cache.insert(identifier, value).await;
}
/// 获取网络缓存
pub async fn get_network(
&self,
container_id: &str,
) -> Option<Option<Arc<HashMap<String, String>>>> {
self.network_cache.get(container_id).await
}
/// 写入网络缓存(支持 None 值)
pub async fn insert_network(
&self,
container_id: String,
value: Option<Arc<HashMap<String, String>>>,
) {
self.network_cache.insert(container_id, value).await;
}
/// 使缓存失效
pub async fn invalidate(&self, identifier: &str) {
self.status_cache.invalidate(identifier).await;
self.network_cache.invalidate(identifier).await;
}
/// 使所有相关缓存失效(用于容器生命周期变化后)
pub async fn invalidate_all(&self, identifiers: &[String]) {
for id in identifiers {
self.status_cache.invalidate(id.as_str()).await;
self.network_cache.invalidate(id.as_str()).await;
}
}
}
/// Docker 容器管理器
pub struct DockerManager {
/// Docker 客户端
docker: Docker,
/// 管理器配置
config: DockerManagerConfig,
/// 容器状态句柄Actor 模式,无锁并发安全)
containers: ContainerStateHandle,
/// 主网络名称(动态检测或使用默认值)
main_network_name: std::sync::Arc<tokio::sync::RwLock<String>>,
/// Docker API 缓存
api_cache: Arc<DockerApiCache>,
}
impl DockerManager {
/// 创建新的 Docker 管理器
pub async fn new(config: DockerManagerConfig) -> DockerResult<Self> {
let docker = if let Some(host) = &config.docker_host {
Docker::connect_with_http(host, 120, API_DEFAULT_VERSION)?
} else {
Docker::connect_with_local_defaults()?
};
// 测试连接
docker.ping().await.map_err(|e| {
DockerError::ConnectionError(format!("unable to connect to Docker daemon: {}", e))
})?;
info!("Docker manager initialized");
// 🔍 动态检测主网络名称(必须成功)
let main_network_name =
match Self::detect_main_network_name_static(&docker, &config.network_base_name).await {
Ok(network_name) => {
info!("detecting network: {}", network_name);
network_name
}
Err(e) => {
error!("unable to detect network: {}", e);
return Err(e);
}
};
// 🆕 创建容器状态 Actor 并启动
let (actor, containers) = ContainerStateActor::new();
tokio::spawn(actor.run());
info!("ContainerStateActor already started");
// 🗄️ 初始化 Docker API 缓存(使用配置的 TTL 和容量)
let api_cache = Arc::new(DockerApiCache::new(
config.cache_status_ttl_seconds,
config.cache_network_ttl_seconds,
config.cache_max_capacity,
));
let manager = Self {
docker,
config,
containers,
main_network_name: std::sync::Arc::new(tokio::sync::RwLock::new(main_network_name)),
api_cache,
};
// 确保 RCoder 网络存在
manager.ensure_rcoder_network().await?;
Ok(manager)
}
/// 使用默认配置创建 Docker 管理器
pub async fn with_default_config() -> DockerResult<Self> {
Self::new(DockerManagerConfig::default()).await
}
/// 带超时的 inspect_container 调用
///
/// 封装 Docker API 调用,添加超时保护,防止请求阻塞
async fn inspect_with_timeout(
&self,
identifier: &str,
timeout: Duration,
) -> DockerResult<bollard::models::ContainerInspectResponse> {
tokio::time::timeout(
timeout,
self.docker
.inspect_container(identifier, None::<InspectContainerOptions>),
)
.await
.map_err(|_| {
DockerError::Timeout(format!(
"Docker API call timeout ({}s): identifier={}",
timeout.as_secs(),
identifier
))
})?
.map_err(DockerError::BollardError)
}
/// 创建并启动容器
pub async fn create_container(
&self,
config: DockerContainerConfig,
) -> DockerResult<DockerContainerInfo> {
info!("startingcreatedcontainer, projectID: {}", config.project_id);
// 生成容器名称:优先使用 pod_id用于容器复用否则使用 project_id
// 当 pod_id 有值时,表示这是多租户场景下的容器复用请求
let container_identifier = config
.pod_id
.as_ref()
.unwrap_or(&config.project_id);
let container_name = super::utils::DockerUtils::generate_container_name(
&config.name_prefix,
container_identifier,
);
// 🔍 先检查 Docker API 中是否存在同名容器
// 如果容器正在运行且 IP 有效,直接复用(避免 pod_id 模式下每次请求都重建容器)
if let Ok(Some(result)) = self.find_container_realtime(&container_name).await {
if result.is_running && !result.container_ip.is_empty() {
info!(
"[CREATE] Container already exists and running, reusing: name={}, id={}, ip={}",
result.container_name, result.container_id, result.container_ip
);
let info = DockerContainerInfo::new(
result.container_id,
result.container_name,
config.project_id.clone(),
config.image.clone(),
);
// 缓存到 DashMap 以便后续查找
self.containers.insert(container_identifier.to_string(), info.clone()).await;
return Ok(info);
}
// 容器已停止,或运行中但 IP 为空(被外部 kill 后网络已销毁)
if result.is_running {
warn!(
"[CREATE] Container running but has empty IP (likely killed externally), deleting: name={}, id={}",
result.container_name, result.container_id
);
} else {
warn!(
"[CREATE] Found stopped container: name={}, id={}, status={:?}, deleting",
result.container_name, result.container_id, result.status
);
}
if let Err(e) = self.stop_container_by_id(&result.container_id).await {
error!("[CREATE] delete container failed: {}", e);
} else {
info!("[CREATE] delete container succeeded");
}
}
// 检查内存缓存并清理
if let Some(existing) = self.containers.get(&config.project_id).await {
warn!(
"Cleaning container record from memory cache: project_id={}, container_name={}",
config.project_id, existing.container_name
);
self.containers.remove(&config.project_id).await;
}
// 拉取镜像(如果本地不存在)
self.ensure_image_exists(&config.image).await?;
// 创建挂载点(按 container_path 去重,避免 Docker Duplicate mount point 错误)
let mut mounts = Vec::new();
let mut mounted_targets: std::collections::HashSet<String> = std::collections::HashSet::new();
// 只在 host_path 非空时添加主挂载点
// 如果为空,表示完全依赖 extra_mounts例如 ComputerAgentRunner
if !config.host_path.is_empty() {
mounted_targets.insert(config.container_path.clone());
mounts.push(Mount {
target: Some(config.container_path.clone()),
source: Some(config.host_path.clone()),
typ: Some(bollard::models::MountType::BIND),
read_only: Some(false),
..Default::default()
});
debug!(
"[DOCKER_MGR] Adding primary mount: {} -> {}",
config.host_path, config.container_path
);
} else {
debug!("📌 [DOCKER_MGR] skip default mount, using extra_mounts config");
}
// 添加额外的挂载点(跳过已存在的 container_path
for extra_mount in &config.extra_mounts {
if !mounted_targets.insert(extra_mount.container_path.clone()) {
warn!(
"[DOCKER_MGR] Skipping duplicate mount target: {} (host: {})",
extra_mount.container_path, extra_mount.host_path
);
continue;
}
mounts.push(Mount {
target: Some(extra_mount.container_path.clone()),
source: Some(extra_mount.host_path.clone()),
typ: Some(bollard::models::MountType::BIND),
read_only: Some(extra_mount.read_only),
..Default::default()
});
}
// 构建环境变量
#[allow(unused_mut)]
let mut env_vars: Vec<String> = config
.env_vars
.into_iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect();
// 🔧 eBPF 调试模式启用自动火焰图生成、Grafana Alloy 持续剖析和 Off-CPU 阻塞分析
#[cfg(feature = "ebpf-debug")]
{
// 通用监控配置(共享)
env_vars.push("SAMPLE_DURATION=30".to_string()); // 采样时长(秒)
env_vars.push("GENERATE_INTERVAL=60".to_string()); // 生成间隔(秒)
env_vars.push("MAX_FLAMEFILES=50".to_string()); // 最大火焰图文件数
env_vars.push("MAX_OFFCPU_FILES=50".to_string()); // 最大 Off-CPU 文件数
// 自动火焰图生成
env_vars.push("ENABLE_EBPF_AUTO_FLAMEGRAPH=true".to_string());
// Grafana Alloy 配置(替代已废弃的 Pyroscope Agent
env_vars.push("ENABLE_ALLOY=true".to_string());
env_vars.push("PYROSCOPE_URL=http://pyroscope:4040".to_string());
// Off-CPU 阻塞分析配置
env_vars.push("ENABLE_OFFCPUTIME=true".to_string());
env_vars.push("OFFCPU_DURATION=30".to_string()); // Off-CPU 采样时长
env_vars.push("OFFCPU_INTERVAL=60".to_string()); // Off-CPU 生成间隔60秒与系统调用监控一致
// 系统调用监控配置
env_vars.push("ENABLE_SYSCALL_MONITOR=true".to_string());
}
// 构建端口映射
let mut port_bindings_map = HashMap::new();
for (container_port, host_port) in &config.port_bindings {
port_bindings_map.insert(
container_port.clone(),
Some(vec![PortBinding {
host_ip: Some("0.0.0.0".to_string()),
host_port: Some(host_port.clone()),
}]),
);
}
// 创建主机配置 - 不再使用 network_mode而是通过 NetworkingConfig 连接到网络
let mut host_config = HostConfig {
mounts: Some(mounts),
port_bindings: Some(port_bindings_map),
auto_remove: Some(config.auto_remove),
// 🔧 容器权限配置:根据 feature 控制特权模式
#[cfg(feature = "ebpf-debug")]
// 调试模式:启用容器特权,允许使用 eBPF 工具
// 通过 make dev-restart 启用此模式
privileged: Some(true),
#[cfg(not(feature = "ebpf-debug"))]
// 生产模式:限制容器权限,提升安全性
privileged: Some(false),
#[cfg(feature = "ebpf-debug")]
cap_add: Some(vec![
"SYS_ADMIN".to_string(), // eBPF 需要
"NET_ADMIN".to_string(), // 网络监控
"SYS_PTRACE".to_string(), // ptrace 追踪
]),
#[cfg(not(feature = "ebpf-debug"))]
cap_drop: Some(vec![
"NET_RAW".to_string(), // 禁止原始套接字(防止 ping、traceroute 等)
"NET_ADMIN".to_string(), // 禁止网络管理(防止修改路由表)
]),
..Default::default()
};
// 应用资源限制
if let Some(ref limits) = config.resource_limits {
host_config.memory = limits.memory_limit.map(|v| v as i64);
host_config.memory_swap = limits.swap_limit.map(|v| v as i64);
// CPU 限制需要通过 nano_cpus 设置 (1 CPU = 1e9 nano CPUs)
if let Some(cpu_limit) = limits.cpu_limit {
host_config.nano_cpus = Some((cpu_limit * 1e9) as i64);
}
}
// 创建容器配置
// 🎯 直接连接到主网络,所有容器共享同一网络以便互相通信
let (networking_config, container_network_name) = if config.network_mode != "host" {
let main_network = self.get_main_network_name().await;
let network_name = config.network_name.as_ref().unwrap_or(&main_network);
let mut endpoints = HashMap::new();
endpoints.insert(
network_name.clone(),
bollard::models::EndpointSettings {
aliases: Some(vec![container_name.clone()]),
..Default::default()
},
);
info!(
"[NETWORK] Container {} connecting to main network: {}",
container_name, network_name
);
(
Some(NetworkingConfig {
endpoints_config: Some(endpoints),
}),
network_name.clone(),
)
} else {
info!(
"🌐 [NETWORK] Container {} uses host network",
container_name
);
(None, "host".to_string())
};
let mut container_config = ContainerCreateBody {
image: Some(config.image.clone()),
working_dir: Some(config.work_dir.clone()),
env: Some(env_vars),
host_config: Some(host_config),
networking_config, // 🎯 直接指定网络配置
tty: Some(true),
open_stdin: Some(true),
// 🔒 设置容器主机名和域名,便于识别和管理
// 注意:这不能阻止容器访问内网 IP只是设置容器的标识
hostname: Some(format!(
"agent-{}",
&config.project_id[..8.min(config.project_id.len())]
)),
domainname: Some("rcoder.local".to_string()),
..Default::default()
};
// 设置启动命令
if let Some(command) = config.command {
container_config.cmd = Some(command);
}
// 设置入口点
if let Some(entrypoint) = config.entrypoint {
container_config.entrypoint = Some(entrypoint);
}
// 创建容器选项
let create_options = CreateContainerOptions {
name: Some(container_name.clone()),
platform: self.config.default_platform.clone(), // 使用配置中的平台
};
// 创建容器
let create_result = self
.docker
.create_container(Some(create_options), container_config)
.await
.map_err(|e| DockerError::ContainerCreationError(format!("failed to create container: {}", e)))?;
let container_id = create_result.id.clone();
// 启动容器
self.docker
.start_container(&container_id, None::<StartContainerOptions>)
.await
.map_err(|e| DockerError::ContainerStartError(format!("failed to start container: {}", e)))?;
// 等待容器启动完成
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
// 检查容器状态,确保容器正在运行
self.check_container_health(&container_id).await?;
// 再次等待确保网络配置完成
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
// 创建容器信息
// 注意:由于容器间通过 Docker 内部网络通信assigned_port 设为 0
// 实际通信使用 container_ip:internal_port
let container_info = DockerContainerInfo {
container_id: container_id.clone(),
container_name: container_name.clone(),
project_id: config.project_id.clone(),
user_id: None, // 将由调用方在 start_agent_container 中设置
service_type: None, // 将由调用方在 start_agent_container 中设置
image: config.image.clone(),
status: ContainerStatus::Running,
created_at: Utc::now(),
started_at: Some(Utc::now()),
host_path: config.host_path.clone(),
container_path: config.container_path.clone(),
port_bindings: config.port_bindings.clone(),
assigned_port: 0, // 内部网络通信,不需要宿主机端口
health_status: None,
service_health: None, // 初始无健康检查结果
internal_port: 8080, // 默认内部端口
network_name: container_network_name.clone(), // 记录使用的网络名称
};
// 保存到容器映射
self.containers
.insert(config.project_id.clone(), container_info.clone())
.await;
info!(
"Container created and started: {} (ID: {}) - connected to network {}",
container_name, container_id, container_network_name
);
Ok(container_info)
}
/// 通过容器ID停止容器
pub async fn stop_container_by_id(&self, container_id: &str) -> DockerResult<()> {
self.stop_container_by_id_with_timeout(container_id, 30)
.await
}
/// 通过容器ID停止容器带超时参数
pub async fn stop_container_by_id_with_timeout(
&self,
container_id: &str,
timeout_seconds: u64,
) -> DockerResult<()> {
info!(
"Quick destroy container: {} (timeout: {}s)",
container_id, timeout_seconds
);
// 先检查容器是否真实存在,避免删除不存在的容器导致错误
// 使用 inspect API 检查容器状态
let timeout = Duration::from_secs(timeout_seconds);
let container_exists = match tokio::time::timeout(
timeout,
self.docker.inspect_container(container_id, None::<InspectContainerOptions>),
)
.await
{
Ok(Ok(_)) => true,
Ok(Err(e)) => {
// 根据 bollard 错误类型判断DockerResponseServerError 包含 status_code
// 404 表示容器不存在,这是正常的(可能已被外部清理)
// 其他错误则记录警告但继续尝试删除
match &e {
bollard::errors::Error::DockerResponseServerError { status_code, .. }
if *status_code == 404 =>
{
info!(
"Container {} does not exist in Docker (status 404, already cleaned up), skipping destroy",
container_id
);
return Ok(());
}
_ => {
warn!(
"Failed to inspect container {} before destroy: {}, will try remove anyway",
container_id, e
);
true // 继续尝试删除
}
}
}
Err(_) => {
// 超时,假设容器可能已不存在,尝试删除
warn!(
"Timeout inspecting container {} ({}s), will try remove anyway",
container_id, timeout_seconds
);
true
}
};
if !container_exists {
info!(
"Container {} does not exist, destroy skipped",
container_id
);
return Ok(());
}
// 🚀 直接使用 force remove无需先 stop
// force: true 会自动停止运行中的容器
// 这样可以避免 "removal already in progress" 的竞态问题
let remove_options = Some(RemoveContainerOptions {
force: true,
v: true,
link: false,
});
self.docker
.remove_container(container_id, remove_options)
.await
.map_err(|e| {
warn!("Failed to remove container {}: {}", container_id, e);
DockerError::BollardError(e)
})?;
info!("containerdestroysucceeded: {}", container_id);
// 从映射中移除(如果存在)- 通过遍历查找 project_id
for info in self.containers.list().await {
if info.container_id == container_id {
self.containers.remove(&info.project_id).await;
// 🔧 使缓存失效
self.api_cache.invalidate(container_id).await;
self.api_cache
.invalidate(info.container_name.as_str())
.await;
break;
}
}
Ok(())
}
/// 停止并删除容器
pub async fn stop_container(&self, project_id: &str) -> DockerResult<()> {
info!("stoppedcontainer, projectID: {}", project_id);
let container_info = if let Some(info) = self.containers.get(project_id).await {
info
} else {
warn!("Project {} has no container", project_id);
return Ok(());
};
// 调用通过ID停止的方法已包含缓存失效和映射移除
self.stop_container_by_id(&container_info.container_id)
.await?;
// 从映射中移除
self.containers.remove(project_id).await;
Ok(())
}
/// 通过 project_id 从缓存中获取容器信息
///
/// 从内存缓存中查询容器信息,速度快但可能不是最新状态。
///
/// # 参数
/// * `project_id` - 项目 IDRCoder 模式的主键)
///
/// # 返回
/// * 如果缓存中存在,返回 `Some(DockerContainerInfo)`
/// * 如果缓存中不存在,返回 `None`
///
/// # 注意
/// - 此方法从缓存查询,容器信息可能不是最新的
/// - 如果需要最新的容器状态,请使用 [`get_container_info_by_name`](Self::get_container_info_by_name)
pub async fn get_container_info(&self, project_id: &str) -> Option<DockerContainerInfo> {
self.containers.get(project_id).await
}
/// 清理容器缓存
///
/// 从 DockerManager 的内存缓存中移除容器信息。
/// 通常在容器被销毁后调用,以保持缓存与实际状态同步。
pub async fn remove_container_cache(&self, project_id: &str) -> Option<DockerContainerInfo> {
self.containers.remove(project_id).await
}
/// 通过多种方式查找容器project_id 或容器名称
///
/// # ⚠️ 已废弃
///
/// 此方法返回的容器信息可能包含过期的 `container_id`。
///
/// **推荐使用**
/// - [`find_container_realtime`](Self::find_container_realtime) - 实时查询 Docker API获取最新的容器信息和 ID
///
/// **问题**
/// - 返回的 `container_id` 可能是缓存中的旧值
/// - 容器重启后 ID 会变化,导致使用旧 ID 操作失败404 错误)
///
/// **迁移指南**
/// ```text
/// // ❌ 旧方式(可能使用过期的 container_id
/// if let Some(info) = docker_manager.find_container_by_identifier("container_name").await {
/// docker_manager.stop_container_by_id(&info.container_id).await?;
/// }
///
/// // ✅ 新方式(获取最新的 container_id
/// if let Ok(Some((container_id, _, _, _))) =
/// docker_manager.find_container_realtime("container_name").await
/// {
/// docker_manager.stop_container_by_id(&container_id).await?;
/// }
/// ```
#[deprecated(
since = "0.1.0",
note = "返回的 container_id 可能过期。请使用 find_container_realtime() 获取最新的容器信息"
)]
pub async fn find_container_by_identifier(
&self,
identifier: &str,
) -> Option<DockerContainerInfo> {
// 1. 首先尝试通过 project_id 查找
if let Some(info) = self.containers.get(identifier).await {
return Some(info);
}
// 2. 如果没找到,尝试通过容器名称查找
for info in self.containers.list().await {
if info.container_name == identifier {
return Some(info);
}
}
// 3. 如果还没找到,尝试通过 Docker API 直接查找容器(适用于容器存在但映射缺失的情况)
let options = Some(ListContainersOptions {
all: true,
..Default::default()
});
if let Ok(containers) = self.docker.list_containers(options).await {
for container in containers {
if let Some(names) = container.names {
for name in names {
// Docker 容器名称通常以 '/' 开头,需要去掉
let clean_name = name.trim_start_matches('/');
if clean_name == identifier {
let container_id = container.id.clone().unwrap_or_default();
info!(
"Found container via Docker API: {} (ID: {})",
identifier, container_id
);
// 🛡️ 从容器信息中获取真实的创建时间
// 使用统一的时间戳解析函数
let created_at = if let Some(created_timestamp) = container.created {
// list_containers API 返回的是 Unix 秒时间戳
Self::parse_unix_timestamp(
created_timestamp,
&format!("container {}", clean_name),
)
.unwrap_or_else(|e| {
warn!(
"parse container created failed: {}, retry with current time",
e
);
Utc::now()
})
} else {
warn!(
"Container missing creation time info, using current time as fallback: container_id={}",
container_id
);
Utc::now()
};
// 创建一个临时的容器信息,用于销毁
return Some(DockerContainerInfo {
container_id,
container_name: clean_name.to_string(),
project_id: "unknown".to_string(), // 我们无法直接知道 project_id
user_id: None,
service_type: None,
image: container.image.unwrap_or_default(),
status: ContainerStatus::Unknown(
"found_via_docker_api".to_string(),
),
created_at,
started_at: None,
host_path: String::new(),
container_path: String::new(),
port_bindings: std::collections::HashMap::new(),
assigned_port: 0,
health_status: None,
service_health: None,
internal_port: 0,
network_name: "unknown".to_string(), // 临时容器信息,网络名称未知
});
}
}
}
}
}
None
}
/// 获取所有容器信息
pub async fn list_containers(&self) -> Vec<DockerContainerInfo> {
self.containers.list().await
}
/// 检查指定ID的容器是否正在运行
pub async fn is_container_running(&self, container_id: &str) -> DockerResult<bool> {
match self
.docker
.inspect_container(container_id, None::<InspectContainerOptions>)
.await
{
Ok(details) => {
if let Some(state) = details.state
&& let Some(status) = state.status
{
return Ok(status == bollard::models::ContainerStateStatusEnum::RUNNING);
}
Ok(false)
}
Err(bollard::errors::Error::DockerResponseServerError {
status_code: 404, ..
}) => {
// 容器不存在,安全地返回 false
Ok(false)
}
Err(e) => {
// 其他类型的错误,作为错误返回
Err(DockerError::BollardError(e))
}
}
}
/// 实时查询容器状态(使用缓存 + 超时保护)
///
/// 与 `find_container_by_identifier` 不同,此方法跳过内存缓存,
/// 直接查询 Docker API 获取最新的容器状态。
///
/// 🔧 优化:使用 Moka 缓存减少 Docker API 调用,使用超时保护防止阻塞
/// 📝 缓存策略:同时缓存 container_id 和 container_name支持 404 响应缓存
///
/// # 参数
/// * `identifier` - 容器名称或容器 ID
///
/// # 返回
/// * 如果找到容器,返回 `Some(ContainerQueryResult)`
/// * 如果容器不存在,返回 `None`
pub async fn find_container_realtime(
&self,
identifier: &str,
) -> DockerResult<Option<ContainerQueryResult>> {
debug!(
"[REALTIME] Getting container status: identifier={}",
identifier
);
// 1. 尝试从缓存获取(只缓存成功结果,不缓存 404
if let Some(Some(cached)) = self.api_cache.get_status(identifier).await {
debug!("[REALTIME] cache hit: identifier={}", identifier);
// Arc::clone 只是增加引用计数,开销很小
return Ok(Some((*cached).clone()));
}
// 2. 缓存未命中,调用 Docker API带超时
let timeout = Duration::from_secs(self.config.api_timeout_quick_seconds);
let result = match self.inspect_with_timeout(identifier, timeout).await {
Ok(details) => {
// 解析结果
let container_id = details.id.unwrap_or_default();
let container_name = details
.name
.map(|n| n.trim_start_matches('/').to_string())
.unwrap_or_else(|| identifier.to_string());
let (status, is_running) = if let Some(state) = details.state {
if let Some(state_status) = state.status {
let is_running =
state_status == bollard::models::ContainerStateStatusEnum::RUNNING;
let status = ContainerStatus::from(state_status.to_string());
(status, is_running)
} else {
(ContainerStatus::Unknown("no status".to_string()), false)
}
} else {
(ContainerStatus::Unknown("no state".to_string()), false)
};
// 🔧 获取容器 IP用于 gRPC 连接)
let container_ip = match self
.get_container_network_info(&container_id)
.await
{
Ok(network_ips) => {
let network_name = self.get_main_network_name().await;
network_ips
.get(&network_name)
.cloned()
.or_else(|| network_ips.values().next().cloned())
.unwrap_or_default()
}
Err(e) => {
warn!(
"[REALTIME] Failed to get container IP, will retry later: container_id={}, error={}",
container_id, e
);
String::new()
}
};
// 🔧 使用 Arc 包装,减少 clone 开销
let query_result = ContainerQueryResult::new(
container_id.clone(),
container_name.clone(),
status,
is_running,
container_ip,
);
let result_arc = Arc::new(query_result);
// 同时用 container_id 和 container_name 作为缓存 key
// Arc::clone 只是增加引用计数,开销很小
self.api_cache
.insert_status(container_id.clone(), Some(result_arc.clone()))
.await;
self.api_cache
.insert_status(container_name.clone(), Some(result_arc.clone()))
.await;
info!(
"[REALTIME] Container status query succeeded: id={}, name={}, status={:?}, running={}, ip={}",
container_id, container_name, result_arc.status, result_arc.is_running, result_arc.container_ip
);
// 返回解引用后的值(因为返回类型不是 Arc
Some((*result_arc).clone())
}
Err(DockerError::BollardError(bollard::errors::Error::DockerResponseServerError {
status_code: 404,
..
})) => {
// 🔧 修复:不再缓存 404 响应
// 原因:容器可能刚被创建,缓存 404 会导致 SSE 连接时序问题
// 容器状态变化快404 缓存收益小但风险大
// 同时清理 network_cache 避免残留旧 IP容器重建后 IP 可能变化)
self.api_cache.invalidate(identifier).await;
debug!(
"[REALTIME] Container does not exist (not caching 404): identifier={}",
identifier
);
None
}
Err(DockerError::Timeout(_)) => {
warn!(
"[REALTIME] Query timeout, trying to get from cache: identifier={}",
identifier
);
// 超时时,尝试返回缓存中的旧值(如果有的话)
if let Some(Some(cached)) = self.api_cache.get_status(identifier).await {
return Ok(Some((*cached).clone()));
}
return Err(DockerError::Timeout(format!(
"Container status query timeout with no available cache: identifier={}",
identifier
)));
}
Err(e) => {
error!(
"[REALTIME] Query container status failed: identifier={}, error={}",
identifier, e
);
return Err(e);
}
};
Ok(result)
}
/// 通过容器名称获取容器创建时间
///
/// 直接查询 Docker API 获取容器的创建时间,不使用缓存。
/// 主要用于容器保护期检查,确保刚创建的容器不会被误清理。
///
/// # 参数
/// * `container_name` - 容器名称
///
/// # 返回
/// * 如果找到容器,返回 `Some(created_time)`
/// * 如果容器不存在,返回 `None`
/// * 如果解析时间失败,返回错误
///
/// # 示例
/// ```ignore
/// let created = docker_manager
/// .get_container_creation_time_by_name("rcoder-agent-123")
/// .await?;
/// if let Some(time) = created {
/// let age = Utc::now().signed_duration_since(time);
/// if age.num_seconds() < protection_seconds {
/// // 在保护期内,跳过清理
/// }
/// }
/// ```
pub async fn get_container_creation_time_by_name(
&self,
container_name: &str,
) -> DockerResult<Option<DateTime<Utc>>> {
debug!(
"[DOCKER_MGR] Querying container creation time: container_name={}",
container_name
);
match self
.docker
.inspect_container(container_name, None::<InspectContainerOptions>)
.await
{
Ok(details) => {
if let Some(ref created_str) = details.created {
match Self::parse_rfc3339_timestamp(
created_str,
&format!("container {}", container_name),
) {
Ok(created_time_utc) => {
debug!(
"[DOCKER_MGR] Container creation time: container_name={}, created={}",
container_name, created_time_utc
);
Ok(Some(created_time_utc))
}
Err(e) => {
error!(
"[DOCKER_MGR] Failed to parse container creation time: container_name={}, error={}",
container_name, e
);
Err(DockerError::InvalidTimestamp(e))
}
}
} else {
warn!(
"[DOCKER_MGR] Container creation time field is empty: container_name={}",
container_name
);
Ok(None)
}
}
Err(bollard::errors::Error::DockerResponseServerError {
status_code: 404, ..
}) => {
// Container does not exist
debug!(
"[DOCKER_MGR] Container does not exist: container_name={}",
container_name
);
Ok(None)
}
Err(e) => {
error!(
"[DOCKER_MGR] Query container info failed: container_name={}, error={}",
container_name, e
);
Err(DockerError::BollardError(e))
}
}
}
/// 解析 RFC3339 时间戳字符串
///
/// 内部辅助函数,统一处理 Docker API 返回的 RFC3339 时间戳解析
///
/// # 参数
/// * `timestamp_str` - RFC3339 格式的时间戳字符串
/// * `context` - 上下文描述(用于日志)
///
/// # 返回
/// * `Ok(DateTime<Utc>)` - 解析成功
/// * `Err(String)` - 解析失败,返回错误描述
fn parse_rfc3339_timestamp(
timestamp_str: &str,
context: &str,
) -> Result<DateTime<Utc>, String> {
DateTime::parse_from_rfc3339(timestamp_str)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| {
format!(
"Failed to parse RFC3339 timestamp for {}: '{}', error: {}",
context, timestamp_str, e
)
})
}
/// 解析 Unix 秒时间戳
///
/// 内部辅助函数,统一处理 Docker API 返回的 Unix 秒时间戳解析
/// 用于 `list_containers` API 返回的 created 字段
///
/// # 参数
/// * `timestamp_secs` - Unix 秒时间戳
/// * `context` - 上下文描述(用于日志)
///
/// # 返回
/// * `Ok(DateTime<Utc>)` - 解析成功
/// * `Err(String)` - 解析失败,返回错误描述
///
/// # 注意
/// Docker 的 list_containers API 返回的是 Unix **秒**时间戳,不是毫秒
fn parse_unix_timestamp(timestamp_secs: i64, context: &str) -> Result<DateTime<Utc>, String> {
DateTime::from_timestamp(timestamp_secs, 0).ok_or_else(|| {
format!(
"Failed to parse Unix timestamp for {}: {} (out of range)",
context, timestamp_secs
)
})
}
/// 通过容器名称从 Docker API 获取完整容器信息
///
/// 直接查询 Docker API 获取最新的容器信息,不使用缓存。
/// 返回完整的 DockerContainerInfo 结构,包含所有容器元数据。
///
/// # 参数
/// * `container_name` - 容器名称
///
/// # 返回
/// * 如果找到容器,返回 `Some(DockerContainerInfo)`
/// * 如果容器不存在,返回 `None`
///
/// # 示例
/// ```ignore
/// if let Some(info) = docker_manager
/// .get_container_info_by_name("rcoder-agent-123")
/// .await?
/// {
/// println!("containerstatus: {:?}, created message : {}", info.status, info.created_at);
/// }
/// ```
///
/// # 与其他方法的对比
/// - [`get_container_info`](Self::get_container_info): 通过 project_id 从缓存查询(快速但可能过期)
/// - [`find_container_realtime`](Self::find_container_realtime): 返回简化信息(只有 id/name/status
/// - **此方法**: 通过 name 查询完整信息(最新数据)
pub async fn get_container_info_by_name(
&self,
container_name: &str,
) -> DockerResult<Option<DockerContainerInfo>> {
debug!(
"[DOCKER_MGR] Querying full info by container name: container_name={}",
container_name
);
match self
.docker
.inspect_container(container_name, None::<InspectContainerOptions>)
.await
{
Ok(details) => {
// 解析容器 ID
let container_id = details
.id
.ok_or_else(|| DockerError::ConfigurationError("Container ID is empty".to_string()))?;
// 解析容器名称(去除前导斜杠)
let name = details
.name
.map(|n| n.trim_start_matches('/').to_string())
.unwrap_or_else(|| container_name.to_string());
// 解析状态和启动时间
let (status, started_at) = if let Some(state) = details.state {
let status_str = state
.status
.map(|s| s.to_string())
.unwrap_or_else(|| "unknown".to_string());
// 使用统一的时间解析函数
let started = state
.started_at
.and_then(|s| Self::parse_rfc3339_timestamp(&s, "started_at").ok());
(ContainerStatus::from(status_str), started)
} else {
(ContainerStatus::Unknown("no state".to_string()), None)
};
// 解析创建时间 - 使用统一的时间解析函数
let created_at = details
.created
.ok_or_else(|| {
DockerError::InvalidTimestamp("Container missing created field".to_string())
})
.and_then(|s| {
Self::parse_rfc3339_timestamp(&s, "created")
.map_err(|e| DockerError::InvalidTimestamp(e))
})?;
// 解析镜像
let image = details
.config
.as_ref()
.and_then(|c| c.image.clone())
.unwrap_or_default();
// 解析挂载信息(查找工作目录绑定)
let (host_path, container_path) = details
.mounts
.as_ref()
.and_then(|mounts| {
mounts.iter().find(|m: &&bollard::models::MountPoint| {
m.typ.as_deref() == Some("bind")
})
})
.and_then(|mount| {
let source = mount.source.clone()?;
let destination = mount.destination.clone()?;
Some((source, destination))
})
.unwrap_or_else(|| (String::new(), String::new()));
// 解析网络和端口信息
let (network_name, port_bindings, assigned_port) =
if let Some(ref network_settings) = details.network_settings {
// 解析网络名称
let net_name = network_settings
.networks
.as_ref()
.and_then(|networks| networks.keys().next().cloned())
.unwrap_or_default();
// 解析端口映射
let mut ports = HashMap::new();
let mut assigned = 0u16;
if let Some(ref port_map) = network_settings.ports {
for (container_port, host_bindings) in port_map {
if let Some(bindings) = host_bindings {
for binding in bindings {
if let Some(ref host_port) = binding.host_port {
ports.insert(container_port.clone(), host_port.clone());
// 尝试解析为数字端口
if assigned == 0 {
if let Ok(port) = host_port.parse::<u16>() {
assigned = port;
}
}
}
}
}
}
}
(net_name, ports, assigned)
} else {
(String::new(), HashMap::new(), 0u16)
};
// 从 Labels 中提取 project_id, user_id, service_type
let labels = details.config.as_ref().and_then(|c| c.labels.as_ref());
let project_id = labels
.and_then(|l| l.get("project_id"))
.cloned()
.unwrap_or_default();
let user_id = labels.and_then(|l| l.get("user_id")).cloned();
let service_type = labels
.and_then(|l| l.get("service_type"))
.and_then(|s| s.parse().ok()); // 使用 FromStr trait
// 内部端口(默认)
let internal_port = match service_type {
Some(shared_types::ServiceType::RCoder) => shared_types::GRPC_DEFAULT_PORT,
Some(shared_types::ServiceType::ComputerAgentRunner) => {
shared_types::HTTP_DEFAULT_PORT
}
None => shared_types::GRPC_DEFAULT_PORT,
};
let info = DockerContainerInfo {
container_id,
container_name: name,
project_id,
user_id,
service_type,
image,
status,
created_at,
started_at,
host_path,
container_path,
port_bindings,
assigned_port,
health_status: None,
service_health: None,
internal_port,
network_name,
};
debug!(
"[DOCKER_MGR] Container info query succeeded: name={}, id={}, status={:?}",
info.container_name, info.container_id, info.status
);
Ok(Some(info))
}
Err(bollard::errors::Error::DockerResponseServerError {
status_code: 404, ..
}) => {
// Container does not exist
debug!(
"[DOCKER_MGR] Container does not exist: container_name={}",
container_name
);
Ok(None)
}
Err(e) => {
error!(
"[DOCKER_MGR] Query container info failed: container_name={}, error={}",
container_name, e
);
Err(DockerError::BollardError(e))
}
}
}
/// 启动 Agent 容器(全流程封装)
///
/// 替代 rcoder 层的复杂编排逻辑
pub async fn start_agent_container(
&self,
params: container_runtime_api::ContainerCreateParams,
) -> DockerResult<ContainerBasicInfo> {
let container_runtime_api::ContainerCreateParams {
project_id,
user_id,
host_workspace_path,
service_type,
resource_limits: request_resource_limits,
pod_id,
isolation_type,
tenant_id,
space_id,
} = params;
info!(
"Starting Agent container: project_id={:?}, user_id={:?}, type={:?}, host_path={}, pod_id={:?}, isolation_type={:?}",
project_id, user_id, service_type, host_workspace_path, pod_id, isolation_type
);
// 1. 在宿主机上预创建工作目录
// 1. 检查工作目录是否已存在(通过绑定挂载,容器内创建会自动同步)
debug!("[DOCKER_MGR] checkworkdirectory: {}", host_workspace_path);
// 绑定挂载机制:容器内创建目录会自动同步到宿主机
// 所以这里不需要额外创建目录
// 2. 清理旧容器(如果提供了 project_id
if let Some(ref id) = project_id
&& let Some(existing) = self.get_container_info(id).await
{
warn!(
"Stopping container {}, already stopped...",
existing.container_name
);
self.stop_container(id).await?;
}
// 2. 获取配置和镜像
let service_config = self.get_service_config(&service_type).await?;
let image = self.select_image(&service_type, None).await?;
// 3. 准备配置
use crate::container_builder::ContainerConfigBuilder;
// 确定用于构建容器配置的主 ID
// 标准 RCoder 使用 project_idComputer Agent Runner 使用 user_id
let container_id: String = if let Some(ref uid) = user_id {
// Computer Agent Runner 使用 user_id
uid.clone()
} else if let Some(ref pid) = project_id {
// 标准 RCoder 使用 project_id
pid.clone()
} else {
// 错误:至少需要提供 project_id 或 user_id 其中一个
return Err(DockerError::ConfigurationError(
"Must provide at least one of project_id or user_id".to_string(),
));
};
// 解析容器内工作目录路径
let mut variables = std::collections::HashMap::new();
// 根据服务类型设置相应的变量
if let Some(ref pid) = project_id {
variables.insert("project_id".to_string(), pid.clone());
}
if let Some(ref uid) = user_id {
variables.insert("user_id".to_string(), uid.clone());
}
variables.insert("service_type".to_string(), service_type.to_string());
// 添加隔离类型相关变量(用于挂载路径解析)
if let Some(ref pid) = pod_id {
variables.insert("pod_id".to_string(), pid.clone());
}
if let Some(ref it) = isolation_type {
variables.insert("isolation_type".to_string(), it.clone());
}
if let Some(ref tid) = tenant_id {
variables.insert("tenant_id".to_string(), tid.clone());
}
if let Some(ref sid) = space_id {
variables.insert("space_id".to_string(), sid.clone());
}
let container_work_path = service_config.resolve_container_path(&variables);
// 构建基础配置
let mut builder = ContainerConfigBuilder::new(container_id.clone())
.image(image)
.name_prefix(service_config.container_prefix())
.work_dir(service_config.work_dir.clone())
.network_mode(service_config.network_mode.clone())
.auto_remove(true);
// 添加隔离类型相关配置
if let Some(ref pid) = pod_id {
builder = builder.pod_id(pid.clone());
}
if let Some(ref it) = isolation_type {
builder = builder.isolation_type(it.clone());
}
// 保存引用供后续使用
let tenant_id_ref = tenant_id.as_deref();
let space_id_ref = space_id.as_deref();
if let Some(tid) = tenant_id_ref {
builder = builder.tenant_id(tid);
}
if let Some(sid) = space_id_ref {
builder = builder.space_id(sid);
}
// 只在 host_workspace_path 非空时添加主挂载点
// 如果为空,表示完全依赖 mounts 配置(例如 ComputerAgentRunner
if !host_workspace_path.is_empty() {
builder = builder
.host_path(host_workspace_path.to_string())
.container_path(container_work_path.clone());
debug!(
"📌 [DOCKER_MANAGER] Main mount: {} -> {}",
host_workspace_path, container_work_path
);
} else {
debug!(
"📌 [DOCKER_MANAGER] Skip mount, no mounts config"
);
}
// 先获取借用字段,因为后续字段会被移动
let container_prefix = service_config.container_prefix().to_string();
let workspace_resolution = service_config.effective_workspace_resolution_path();
let workspace_container = service_config.workspace_container_path();
// 应用资源限制
let limits = service_config.resource_limits;
// 合并资源限制:请求级别覆盖服务级别
let final_resource_limits = match request_resource_limits {
Some(request_limits) => {
// 再次验证(防御性编程)
request_limits.validate().map_err(|e| {
DockerError::ConfigurationError(format!("Invalid resource limits: {}", e))
})?;
// 合并配置
limits.merge_with(&request_limits)
}
None => limits,
};
builder = builder.resource_limits(crate::types::ResourceLimits {
memory_limit: final_resource_limits.memory_limit,
cpu_limit: final_resource_limits.cpu_limit,
swap_limit: final_resource_limits.swap_limit,
});
// 添加环境变量
// 处理其他环境变量中的模板(先处理,因为后续需要使用 project_id/user_id 的值)
for (key, value) in &service_config.environment {
let mut processed_value = value.clone();
if let Some(ref pid) = project_id {
processed_value = processed_value.replace("{project_id}", pid);
}
if let Some(ref uid) = user_id {
processed_value = processed_value.replace("{user_id}", uid);
}
builder = builder.env(key, &processed_value);
}
// 根据服务类型设置相应的环境变量(最后设置,覆盖模板处理的值)
if let Some(ref pid) = project_id {
builder = builder.env("PROJECT_ID", pid);
}
if let Some(ref uid) = user_id {
builder = builder.env("USER_ID", uid);
}
// 隔离模式相关环境变量agent_runner 用于构建工作目录路径)
if let Some(ref tid) = tenant_id {
builder = builder.env("TENANT_ID", tid);
}
if let Some(ref sid) = space_id {
builder = builder.env("SPACE_ID", sid);
}
if let Some(ref it) = isolation_type {
builder = builder.env("ISOLATION_TYPE", it);
}
// 注意:子容器以 root 用户运行,不再需要 UID/GID 匹配
// 设置网络
let network_name = self.get_main_network_name().await;
builder = builder.network_name(network_name);
// ===== 自动注入 workspace 挂载 =====
// workspace_resolution: rcoder 容器内路径 → 解析宿主机路径
// workspace_container: sub-container 内挂载目标
//
// auto-inject 统一处理 workspace 隔离挂载config mounts 中的 workspace 挂载
// 通过 resolve_from 精确匹配跳过,避免重复。
// 记录 auto-inject 已添加的 container_path用于后续冲突检测
let mut auto_injected_paths: std::collections::HashSet<String> = std::collections::HashSet::new();
match crate::path::resolve_container_path_to_host(
std::path::Path::new(&workspace_resolution),
)
.await
{
Ok(workspace_host_path) => {
let (host_sub, container_mount) = if pod_id.is_some() {
// pod_id 有值:根据 isolation_type 决定挂载级别
let tid = tenant_id.as_deref().unwrap_or("default");
let sid = space_id.as_deref().unwrap_or("default");
let proj = project_id.as_deref().unwrap_or("default");
match isolation_type.as_deref().map(|s| s.to_lowercase()) {
Some(ref it) if it == "space" => {
let sub = format!("{}/{}", tid, sid);
(sub.clone(), std::path::PathBuf::from(&workspace_container).join(&sub))
}
Some(ref it) if it == "tenant" => {
let sub = format!("{}", tid);
(sub.clone(), std::path::PathBuf::from(&workspace_container).join(&sub))
}
_ => {
let sub = format!("{}/{}/{}", tid, sid, proj);
(sub.clone(), std::path::PathBuf::from(&workspace_container).join(&sub))
}
}
} else {
// pod_id 无值:根据 service_type 选择挂载策略
match service_type {
// ComputerAgentRunner: 一个 user_id 对应一个容器
// 挂载: 宿主机 /computer-project-workspace/{user_id} → 容器 /home/user
// config.yml 中 container_path: "/home/user"
ServiceType::ComputerAgentRunner => {
let uid = user_id.as_deref().unwrap_or("default");
(uid.to_string(), std::path::PathBuf::from(&workspace_container))
}
// RCoder: 一个 project_id 对应一个容器
// 挂载: 宿主机 /project_workspace/{project_id} → 容器 /project_workspace/{project_id}
ServiceType::RCoder => {
let pid = project_id.as_deref().unwrap_or("default");
(pid.to_string(), std::path::PathBuf::from(&workspace_container).join(pid))
}
}
};
let host_mount = workspace_host_path.join(&host_sub);
// 创建目录在容器内创建bind mount 传播到宿主机)
std::fs::create_dir_all(&container_mount).ok();
let host_mount_str = host_mount.to_string_lossy().to_string();
let container_mount_str = container_mount.to_string_lossy().to_string();
auto_injected_paths.insert(container_mount_str.clone());
builder = builder.add_mount(crate::MountPoint {
host_path: host_mount_str,
container_path: container_mount_str,
read_only: false,
});
info!(
"📌 [DOCKER_MGR] Auto workspace mount: {} -> {}",
host_mount.display(), container_mount.display()
);
}
Err(e) => {
warn!(
"⚠️ [DOCKER_MGR] Failed to resolve workspace host path, skipping auto mount: {}",
e
);
}
}
// 🎯 处理配置文件中的挂载点 (service_config.mounts)
let container_name = format!("{}-{}", container_prefix, container_id);
let timestamp = Utc::now().format("%Y%m%d%H%M%S").to_string();
let log_dir_name = format!("{}-{}", container_name, timestamp);
// 设置命令(放在获取 container_name 之后,因为会移动 service_config.command
builder = builder.command(service_config.command);
if let Some(entry) = service_config.entrypoint {
builder = builder.entrypoint(entry);
}
// 基础变量集
let mut base_variables = variables.clone();
base_variables.insert("container_name".to_string(), container_name.clone());
base_variables.insert("timestamp".to_string(), timestamp.clone());
base_variables.insert("log_dir_name".to_string(), log_dir_name.clone());
// 缓存已解析的路径,避免重复解析
let mut resolved_paths_cache: std::collections::HashMap<String, std::path::PathBuf> =
std::collections::HashMap::new();
// 添加配置文件中定义的挂载点
for mount_config in &service_config.mounts {
// 跳过 workspace 挂载auto-inject 统一处理隔离挂载
// 精确匹配 resolve_from不靠路径模式猜测
if mount_config.resolve_from.as_deref() == Some(&workspace_resolution) {
debug!(
"Skipping workspace mount from config (auto-inject handles isolation): \
container_path={}, resolve_from={}",
mount_config.container_path,
mount_config.resolve_from.as_deref().unwrap_or("")
);
continue;
}
let mut mount_variables = base_variables.clone();
// 如果配置了 resolve_from解析动态路径
if let Some(ref resolve_from_path) = mount_config.resolve_from {
// 检查缓存(只缓存基础路径解析结果)
let resolved_base =
if let Some(cached) = resolved_paths_cache.get(resolve_from_path) {
Some(cached.clone())
} else {
// 解析 resolve_from 路径到宿主机基础路径
match crate::path::resolve_container_path_to_host(std::path::Path::new(
resolve_from_path,
))
.await
{
Ok(host_base_path) => {
info!(
"[DOCKER_MGR] Resolved from {} to host path: {}",
resolve_from_path,
host_base_path.display()
);
// 缓存基础路径解析结果
resolved_paths_cache
.insert(resolve_from_path.clone(), host_base_path.clone());
Some(host_base_path)
}
Err(e) => {
warn!(
"[DOCKER_MGR] Unable to resolve path (resolve_from: {}): {}",
resolve_from_path, e
);
None
}
}
};
// 添加解析后的基础路径变量
if let Some(resolved_path) = resolved_base {
let normalized = resolved_path.components().collect::<std::path::PathBuf>();
mount_variables.insert(
"resolved_path".to_string(),
normalized.to_string_lossy().to_string(),
);
} else {
// 如果解析失败,跳过此挂载点
warn!(
"[DOCKER_MGR] Skipping mount point (unable to resolve resolve_from): {}",
mount_config.container_path
);
continue;
}
}
// 解析宿主机路径中的变量
let resolved_host_path = mount_config.resolve_host_path(&mount_variables);
// 检查是否还有未替换的变量(如 {logs_host_path} 等)
if resolved_host_path.contains('{') && resolved_host_path.contains('}') {
warn!(
"[DOCKER_MGR] Skipping mount point (host_path contains unresolved variables): {}",
resolved_host_path
);
continue;
}
// 使用 PathBuf 规范化路径(消除多余的斜杠)
let resolved_host_path = std::path::PathBuf::from(&resolved_host_path)
.components()
.collect::<std::path::PathBuf>()
.to_string_lossy()
.to_string();
// 解析容器路径中的变量
let mut resolved_container_path = mount_config.container_path.clone();
for (key, value) in &mount_variables {
resolved_container_path =
resolved_container_path.replace(&format!("{{{}}}", key), value);
}
info!(
"Adding mount point: {} -> {} (read_only: {})",
resolved_host_path, resolved_container_path, mount_config.read_only
);
// 确保目录存在(仅对非只读挂载创建目录)
// 重要Docker bind mount 要求宿主机路径必须存在
//
// 由于代码运行在容器内,无法直接访问宿主机路径(如 /Volumes/soddygo/...
// 必须使用容器内通过 volume 挂载可访问的路径来创建目录
//
// 策略:必须配置 resolve_from 才能正确创建目录
// 使用 resolve_from + 相对路径 来创建目录
if !mount_config.read_only {
if let Some(ref resolve_from_path) = mount_config.resolve_from {
// 非 pod 模式:从 host_path 模板中提取相对路径部分
// host_path 格式通常是 "{resolved_path}/{variable}"
// 我们需要取 {resolved_path} 之后的部分,并拼接到 resolve_from 上
let host_path_template = &mount_config.host_path;
let relative_part = if host_path_template.starts_with("{resolved_path}") {
// 提取 {resolved_path} 之后的模板部分并替换变量
let suffix = host_path_template
.strip_prefix("{resolved_path}")
.unwrap_or("");
let mut resolved_suffix = suffix.to_string();
for (key, value) in &mount_variables {
resolved_suffix =
resolved_suffix.replace(&format!("{{{}}}", key), value);
}
resolved_suffix
} else {
// 如果不是以 {resolved_path} 开头,尝试计算相对路径
// 这种情况不太常见,使用空字符串作为后备
String::new()
};
// 构建容器内可访问的路径
let create_path = format!("{}{}", resolve_from_path, relative_part);
debug!(
"Creating directory using container path: {} (resolve_from: {}, relative: {})",
create_path, resolve_from_path, relative_part
);
if let Err(e) = std::fs::create_dir_all(&create_path) {
warn!(
"[DOCKER_MGR] createdmountdirectoryfailed: {} - {}",
create_path, e
);
} else {
info!("[DOCKER_MGR] directorycreatedsucceeded: {}", create_path);
}
} else {
// 没有配置 resolve_from无法在容器内创建宿主机路径
// 假设目录已存在,跳过创建
info!(
"Skipping directory creation (resolve_from not configured): {}",
resolved_host_path
);
}
}
// 冲突检测:如果 config 挂载的 container_path 与 auto-inject 重复,丢弃 config 挂载
if auto_injected_paths.contains(&resolved_container_path) {
warn!(
"[DOCKER_MGR] Skipping config mount (conflicts with auto-inject): \
{} -> {}",
resolved_container_path, resolved_host_path
);
continue;
}
builder = builder.add_mount(crate::MountPoint {
host_path: resolved_host_path,
container_path: resolved_container_path,
read_only: mount_config.read_only,
});
// 如果是日志挂载,添加环境变量
if mount_config.container_path.contains("container-logs") {
builder = builder.env("CONTAINER_LOGS_DIR", &mount_config.container_path);
builder = builder.env("CONTAINER_LOG_NAME", &log_dir_name);
}
}
// 4. 创建并启动
let config = builder
.build()
.map_err(|e| DockerError::ContainerCreationError(e.to_string()))?;
self.create_container(config).await?;
// 🆕 更新容器映射中的 user_id 和 service_type
if let Some(mut info) = self.containers.get(&container_id).await {
info.user_id = user_id.map(|s| s.to_string());
info.service_type = Some(service_type.clone());
debug!(
"📝 [DOCKER_MGR] Updating container metadata: container_id={}, user_id={:?}, service_type={:?}",
container_id, info.user_id, info.service_type
);
self.containers.insert(container_id.to_string(), info.clone()).await;
// 当 pod_id 存在时,也用 pod_id 作为 key 缓存,确保后续请求通过 pod_id 能找到容器
if let Some(ref pid) = pod_id {
self.containers.insert(pid.to_string(), info).await;
debug!(
"📝 [DOCKER_MGR] Cached container under pod_id key: pod_id={}",
pid
);
}
}
// 5. 等待就绪并返回信息
// 优先使用 pod_id 查找(复用场景),否则使用 container_id (project_id)
let lookup_key = pod_id.as_deref().unwrap_or(&container_id);
let info = self.get_agent_info(lookup_key).await?.ok_or_else(|| {
DockerError::ContainerStartError("unable to get info after container started".to_string())
})?;
// 健康检查
crate::health::wait_for_service_ready(&info.service_url)
.await
.map_err(|e| DockerError::ContainerStartError(format!("health check failed: {}", e)))?;
info!("Agent container started: {}", info.service_url);
Ok(info)
}
/// 查找项目容器RCoder 模式专用)
///
/// 根据 project_id 和 service_type 查找容器:
/// - 容器命名规则:`{prefix}-{project_id}`
/// - RCoder 模式前缀:`rcoder-agent`
///
/// # 策略
/// 1. 查找内部 Map (project_id)
/// 2. 实时查询 Docker API (构造容器名称)
///
/// # 返回
/// * `Ok(Some(ContainerQueryResult))` - 容器存在
/// * `Ok(None)` - 容器不存在
/// * `Err(...)` - 查询出错
pub async fn find_project_container(
&self,
project_id: &str,
service_type: &shared_types::ServiceType,
) -> DockerResult<Option<ContainerQueryResult>> {
// 1. 查 DashMap 缓存 (如果存在且运行中,通过容器名查询 IP)
if let Some(info) = self.containers.get(project_id).await {
let is_running = matches!(info.status, ContainerStatus::Running);
if !is_running {
return Ok(Some(ContainerQueryResult::new(
info.container_id.clone(),
info.container_name.clone(),
info.status.clone(),
false,
String::new(),
)));
}
// 容器运行中,通过容器名查询获取 IPMoka API 缓存优先miss 时才调 Docker API
// 如果无法获取 IP容器已被销毁但 DashMap 缓存未清理),标记为非运行状态
let (container_ip, effective_status) = match self
.find_container_realtime(&info.container_name)
.await
{
Ok(Some(realtime_info)) if !realtime_info.container_ip.is_empty() => {
(realtime_info.container_ip, info.status.clone())
}
Ok(Some(realtime_info)) => {
warn!(
"⚠️ [FIND_CONTAINER] Container in DashMap marked Running but has empty IP, treating as stopped: container_name={}, container_id={}",
info.container_name, info.container_id
);
(realtime_info.container_ip, ContainerStatus::Stopped)
}
_ => {
warn!(
"⚠️ [FIND_CONTAINER] Container in DashMap marked Running but not found via Docker API, treating as stopped: container_name={}, container_id={}",
info.container_name, info.container_id
);
(String::new(), ContainerStatus::Stopped)
}
};
let is_running = matches!(effective_status, ContainerStatus::Running);
return Ok(Some(ContainerQueryResult::new(
info.container_id.clone(),
info.container_name.clone(),
effective_status,
is_running,
container_ip,
)));
}
// 2. 实时查询 Docker API (构造名称)
// 使用 service_config.container_prefix() 获取配置的前缀
let prefix = match self.get_service_config(service_type).await {
Ok(config) => config.container_prefix().to_string(),
Err(e) => {
warn!(
"⚠️ [FIND_CONTAINER] Failed to get service config, using default prefix: service_type={:?}, error={}",
service_type, e
);
service_type.container_prefix().to_string()
}
};
let expected_container_name = format!("{}-{}", prefix, project_id);
// 直接返回 find_container_realtime 的结果
self.find_container_realtime(&expected_container_name).await
}
/// 获取 Agent 容器的高级信息
///
/// 封装了容器查找、IP解析、URL构建和信息转换逻辑
/// 替代 rcoder 层的手动拼装逻辑
pub async fn get_agent_info(
&self,
project_id: &str,
) -> DockerResult<Option<ContainerBasicInfo>> {
// 1. 查找容器信息(内存映射)
let container_info = match self.get_container_info(project_id).await {
Some(info) => info,
None => return Ok(None),
};
// 2. 获取容器 IP (优先使用主网络)
// 注意:如果容器已被外部删除(如手动 docker rm此处会出错
let network_name = self.get_main_network_name().await;
let network_ips = match self
.get_container_network_info(&container_info.container_id)
.await
{
Ok(ips) => ips,
Err(e) => {
// 检查是否是容器不存在的错误404 状态码)
// 容器已被外部删除,清理内存映射并返回 None
// 这样上层调用者可以重新创建容器
if matches!(
&e,
DockerError::BollardError(bollard::errors::Error::DockerResponseServerError {
status_code: 404,
..
})
) {
warn!(
"⚠️ [GET_AGENT_INFO] Container was externally deleted (status 404), cleaning up memory mapping: project_id={}, container_id={}",
project_id, container_info.container_id
);
self.containers.remove(project_id).await;
return Ok(None);
}
// 其他错误正常传播
return Err(e);
}
};
// 如果网络信息为空,说明容器可能已被删除或未正确连接到网络
// 清理内存映射并返回 None让上层调用者重新创建容器
if network_ips.is_empty() {
warn!(
"⚠️ [GET_AGENT_INFO] Container has no network info (may have been deleted), cleaning up memory mapping: project_id={}, container_id={}",
project_id, container_info.container_id
);
self.containers.remove(project_id).await;
return Ok(None);
}
let container_ip = network_ips
.get(&network_name)
.cloned()
.or_else(|| network_ips.values().next().cloned())
.ok_or_else(|| DockerError::ConnectionError("Container not connected to any network".to_string()))?;
// 3. 构建服务 URL (Agent 内部默认监听 8086)
let server_url = format!("http://{}:8086", container_ip);
// 4. 转换并返回
Ok(Some(ContainerBasicInfo {
container_id: container_info.container_id,
container_name: container_info.container_name,
container_ip,
internal_port: container_info.internal_port,
external_port: container_info.assigned_port,
project_id: container_info.project_id,
status: container_info.status.to_string(),
created_at: container_info.created_at,
service_url: server_url,
}))
}
/// 获取容器的连接信息 (IP)
///
/// 用于清理任务获取资源回收所需的信息
pub async fn get_container_connection_info(
&self,
container_info: &DockerContainerInfo,
) -> DockerResult<Option<String>> {
// 1. 获取 IP
let ip_addr = match self
.get_container_network_info(&container_info.container_id)
.await
{
Ok(network_ips) => network_ips
.get(&container_info.network_name)
.cloned()
.or_else(|| network_ips.values().next().cloned()),
Err(e) => {
warn!("get container ip failed: {}", e);
None
}
};
Ok(ip_addr)
}
// ========================================================================
// ComputerAgentRunner 专用接口
// ========================================================================
//
// ComputerAgentRunner 模式与 RCoder 模式不同:
// - 容器命名computer-agent-runner-{user_id}(而非 project_id
// - 一个 user_id 对应一个容器
// - 容器内可以运行多个 project_id 的 Agent 实例
//
// 以下接口专门用于 ComputerAgentRunner 模式,参数名更清晰,
// 避免与 RCoder 模式的 project_id 参数混淆。
/// 获取用户容器信息ComputerAgentRunner 模式专用)
///
/// # Arguments
/// * `user_id` - 用户 ID用作容器标识符
///
/// # 说明
/// - ComputerAgentRunner 模式下,一个用户对应一个容器
/// - 容器命名规则:`computer-agent-runner-{user_id}`
/// - 容器内可以运行多个 project_id 的 Agent 实例
///
/// # 返回
/// 容器信息(如果存在),否则返回 None
pub async fn get_user_container_info(
&self,
user_id: &str,
) -> DockerResult<Option<ContainerBasicInfo>> {
// 内部调用 get_agent_info但参数名更清晰
self.get_agent_info(user_id).await
}
/// 查找用户容器ComputerAgentRunner 模式专用)
///
/// 根据 user_id 和 service_type 查找容器:
/// - 容器命名规则:`{prefix}-{user_id}`
/// - ComputerAgentRunner 模式前缀:`computer-agent-runner`
///
/// # Arguments
/// * `user_id` - 用户 ID
/// * `service_type` - 服务类型(应该是 ComputerAgentRunner
///
/// # 返回
/// * `Ok(Some(ContainerQueryResult))` - 容器存在
/// * `Ok(None)` - 容器不存在
/// * `Err(...)` - 查询出错
pub async fn find_user_container(
&self,
user_id: &str,
service_type: &shared_types::ServiceType,
) -> DockerResult<Option<ContainerQueryResult>> {
// 1. 查 Map (如果存在且运行中,直接返回)
if let Some(info) = self.containers.get(user_id).await {
return Ok(Some(ContainerQueryResult::new(
info.container_id.clone(),
info.container_name.clone(),
info.status.clone(),
matches!(info.status, ContainerStatus::Running),
String::new(), // 缓存命中时 IP 可能已过期,依赖后续实时查询更新
)));
}
// 2. 实时查询 Docker API (构造名称)
// 使用 service_config.container_prefix() 获取配置的前缀
let prefix = match self.get_service_config(service_type).await {
Ok(config) => config.container_prefix().to_string(),
Err(e) => {
warn!(
"⚠️ [FIND_CONTAINER] Failed to get service config, using default prefix: service_type={:?}, error={}",
service_type, e
);
service_type.container_prefix().to_string()
}
};
let expected_container_name = format!("{}-{}", prefix, user_id);
// 直接返回 find_container_realtime 的结果
self.find_container_realtime(&expected_container_name).await
}
/// 通过用户 ID 获取容器 IDComputerAgentRunner 模式专用)
///
/// # Arguments
/// * `user_id` - 用户 ID
///
/// # 返回
/// 容器 ID如果存在否则返回 None
pub async fn get_user_container_id(&self, user_id: &str) -> DockerResult<Option<String>> {
// 从容器信息中获取 container_id
Ok(self
.get_container_info(user_id)
.await
.map(|info| info.container_id))
}
/// 检查用户容器是否存在ComputerAgentRunner 模式专用)
///
/// # Arguments
/// * `user_id` - 用户 ID
///
/// # 返回
/// true 如果容器存在且运行中,否则返回 false
pub async fn is_user_container_running(&self, user_id: &str) -> bool {
match self
.find_user_container(user_id, &shared_types::ServiceType::ComputerAgentRunner)
.await
{
Ok(Some(result)) => result.is_running,
_ => false,
}
}
/// 检查并更新容器状态
pub async fn update_container_status(
&self,
project_id: &str,
) -> DockerResult<Option<ContainerStatus>> {
let container_info = if let Some(info) = self.containers.get(project_id).await {
info
} else {
return Ok(None);
};
// 查询容器状态
match self
.docker
.inspect_container(
&container_info.container_id,
None::<InspectContainerOptions>,
)
.await
{
Ok(details) => {
if let Some(state) = details.state {
let status = state
.status
.map(|s| ContainerStatus::from(s.to_string()))
.unwrap_or(ContainerStatus::Unknown("unknown".to_string()));
// 更新状态
let mut info = container_info;
info.status = status.clone();
info.health_status = state.health.and_then(|h| h.status.map(|s| s.to_string()));
self.containers.insert(project_id.to_string(), info).await;
Ok(Some(status))
} else {
Ok(Some(ContainerStatus::Unknown("no state".to_string())))
}
}
Err(bollard::errors::Error::DockerResponseServerError {
status_code: 404, ..
}) => {
// 容器不存在HTTP 404从映射中移除
self.containers.remove(project_id).await;
Ok(None)
}
Err(e) => Err(DockerError::BollardError(e)),
}
}
/// 同步所有缓存容器的状态
///
/// 遍历缓存中的所有容器,调用 Docker API 检查其真实状态。
/// 如果容器已被外部删除(如手动 `docker stop`),则从缓存中移除。
/// 🆕 对运行中的容器执行服务健康检查HTTP + gRPC
///
/// # Returns
/// 返回元组 (已检查数量, 已移除容器信息列表)
pub async fn sync_all_container_states(&self) -> DockerResult<(u32, Vec<RemovedContainerInfo>)> {
// 获取所有 project_id 的快照
let project_ids: Vec<String> = self.containers.keys().await;
if project_ids.is_empty() {
return Ok((0, Vec::new()));
}
let total = project_ids.len() as u32;
let mut removed = Vec::new();
let mut health_checked_count = 0u32;
// 创建健康检查器(复用同一个实例)
let health_checker = crate::health::ServiceHealthChecker::new();
for project_id in project_ids {
// 在调用 update_container_status 之前获取容器信息(用于在容器被移除时构造 RemovedContainerInfo
let container_info_before_update = self.containers.get(&project_id).await;
match self.update_container_status(&project_id).await {
Ok(None) => {
// 容器不存在,已从缓存中移除
if let Some(info) = container_info_before_update {
// 获取容器 IP用于清理 gRPC 连接池)
let container_ip = match self
.get_container_network_info(&info.container_id)
.await
{
Ok(ips) => ips.values().next().cloned().unwrap_or_default(),
Err(e) => {
warn!(
"[SYNC] Failed to get container IP for cleanup: container_id={}, error={}",
info.container_id, e
);
String::new()
}
};
removed.push(RemovedContainerInfo {
container_name: info.container_name,
container_ip,
identifier: project_id.clone(),
service_type: info.service_type.unwrap_or(ServiceType::RCoder),
});
}
info!(
"[SYNC] Container removed from cache (does not exist in Docker): project_id={}",
project_id
);
}
Ok(Some(status)) => {
// 🆕 对运行中的容器执行服务健康检查
if matches!(status, ContainerStatus::Running) {
// Actor 模式:直接异步获取,无锁
let container_info = self.containers.get(&project_id).await;
let Some(container_info) = container_info else {
continue;
};
// 获取容器 IP锁已释放可以安全 await
if let Ok(network_ips) = self
.get_container_network_info(&container_info.container_id)
.await
{
// 优先使用主网络 IP否则使用第一个可用 IP
let network_name = self.get_main_network_name().await;
let container_ip = network_ips
.get(&network_name)
.or_else(|| network_ips.values().next());
if let Some(ip) = container_ip {
// 获取之前的失败次数
let previous_failures = container_info
.service_health
.as_ref()
.map(|h| h.consecutive_failures)
.unwrap_or(0);
// 执行健康检查
let health_status =
health_checker.check_service(ip, previous_failures).await;
// 更新缓存(这里获取新的写锁,不会死锁)
let mut updated_info = container_info.clone();
updated_info.service_health = Some(health_status.clone());
self.containers
.insert(project_id.clone(), updated_info)
.await;
health_checked_count += 1;
if health_status.is_fully_healthy() {
debug!(
"[SYNC] Service healthy: container_id={}, service_type={:?}",
project_id, container_info.service_type
);
} else {
warn!(
"[SYNC] Service unhealthy: container_id={}, service_type={:?}, http={}, grpc={}, failures={}",
project_id,
container_info.service_type,
health_status.http_healthy,
health_status.grpc_healthy,
health_status.consecutive_failures
);
}
}
}
}
}
Err(e) => {
warn!(
"[SYNC] Check container status failed: project_id={}, error={}",
project_id, e
);
}
}
}
if !removed.is_empty() || health_checked_count > 0 {
info!(
"[SYNC] Container status sync completed: checked={}, removed={}, health_checked={}",
total, removed.len(), health_checked_count
);
}
// 清理被移除容器的 API 缓存(避免残留数据导致后续查询返回旧 IP
if !removed.is_empty() {
let identifiers: Vec<String> = removed.iter().map(|r| r.identifier.clone()).collect();
self.api_cache.invalidate_all(&identifiers).await;
debug!(
"[SYNC] Invalidated API cache for {} removed containers",
identifiers.len()
);
}
Ok((total, removed))
}
/// 清理所有容器
pub async fn cleanup_all_containers(&self) -> DockerResult<()> {
info!("Starting cleanup of all containers");
let project_ids: Vec<String> = self.containers.keys().await;
for project_id in project_ids {
if let Err(e) = self.stop_container(&project_id).await {
error!(
"cleanup project {} container failed: {}",
project_id, e
);
}
}
info!("Container cleanup completed");
Ok(())
}
/// 确保镜像存在,如果不存在则拉取
async fn ensure_image_exists(&self, image: &str) -> DockerResult<()> {
debug!("Checking if image exists: {}", image);
// 检查镜像是否存在
match self.docker.inspect_image(image).await {
Ok(_) => {
debug!("Image {} already exists", image);
Ok(())
}
Err(_) => {
info!("Image {} not found, pulling...", image);
let pull_options = CreateImageOptions {
from_image: Some(image.to_string()),
..Default::default()
};
let mut pull_stream = self.docker.create_image(Some(pull_options), None, None);
while let Some(result) = pull_stream.next().await {
match result {
Ok(progress) => {
if let Some(status) = progress.status {
debug!("container status: {}", status);
}
}
Err(e) => {
return Err(DockerError::ImagePullError(format!(
"Failed to pull image: {}",
e
)));
}
}
}
info!("Image {} pull completed", image);
Ok(())
}
}
}
/// 获取容器日志
pub async fn get_container_logs(&self, project_id: &str, lines: i64) -> DockerResult<String> {
let container_info = if let Some(info) = self.containers.get(project_id).await {
info
} else {
return Err(DockerError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("project {} has no corresponding container", project_id),
)));
};
let log_options = LogsOptions {
stdout: true,
stderr: true,
tail: lines.to_string(),
timestamps: true,
..Default::default()
};
let mut log_stream = self
.docker
.logs(&container_info.container_id, Some(log_options));
let mut logs = String::new();
while let Some(result) = log_stream.next().await {
match result {
Ok(output) => {
logs.push_str(&String::from_utf8_lossy(&output.into_bytes()));
}
Err(e) => {
warn!("get container ip failed: {}", e);
}
}
}
Ok(logs)
}
/// 重启容器
pub async fn restart_container(&self, project_id: &str) -> DockerResult<()> {
info!("Creating container, projectID: {}", project_id);
let container_info = if let Some(info) = self.containers.get(project_id).await {
info
} else {
return Err(DockerError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("project {} has no corresponding container", project_id),
)));
};
self.docker
.restart_container(
&container_info.container_id,
None::<RestartContainerOptions>,
)
.await
.map_err(|e| DockerError::ContainerStartError(format!("failed to restart container: {}", e)))?;
// 🔧 使缓存失效(容器状态已变更)
self.api_cache
.invalidate(container_info.container_id.as_str())
.await;
self.api_cache
.invalidate(container_info.container_name.as_str())
.await;
info!(
"Container created: {}",
container_info.container_name
);
Ok(())
}
/// 确保 RCoder 网络存在
async fn ensure_rcoder_network(&self) -> DockerResult<()> {
let main_network = self.get_main_network_name().await;
info!("Checking RCoder network status: {}...", main_network);
// 检查网络是否已存在
match self.inspect_network(&main_network).await {
Ok(_) => {
info!("RCoder network already exists: {}", main_network);
Ok(())
}
Err(_) => {
warn!("RCoder network not found: {}", main_network);
warn!("Network mode: bridge");
warn!("Please check Docker Compose config");
// 不创建网络,因为主网络应该由 Docker Compose 创建
Ok(())
}
}
}
/// 检查网络是否存在
async fn inspect_network(&self, network_name: &str) -> DockerResult<()> {
use bollard::query_parameters::ListNetworksOptions;
// 使用 list_networks 不带参数,然后手动过滤
let networks = self
.docker
.list_networks(None::<ListNetworksOptions>)
.await
.map_err(|e| DockerError::ConnectionError(format!("failed to list networks: {}", e)))?;
if networks
.iter()
.any(|n| n.name.as_ref() == Some(&network_name.to_string()))
{
Ok(())
} else {
Err(DockerError::ConnectionError("Network does not exist".to_string()))
}
}
/// 获取 Docker 客户端实例
pub fn get_docker_client(&self) -> &Docker {
&self.docker
}
/// 获取配置的默认镜像
pub fn get_default_image(&self) -> String {
self.config.default_image.clone()
}
/// 根据服务类型选择镜像
pub async fn select_image(
&self,
service_type: &shared_types::ServiceType,
project_overrides: Option<&shared_types::ProjectImageOverrides>,
) -> DockerResult<String> {
// 使用多镜像配置选择镜像
use crate::image_selector::ImageSelector;
let selector = ImageSelector::new(self.config.multi_image_config.clone());
debug!(" ImageSelector: {:?}", service_type);
selector.select_image(service_type, project_overrides).await
}
/// 获取服务配置
pub async fn get_service_config(
&self,
service_type: &shared_types::ServiceType,
) -> DockerResult<shared_types::ServiceImageConfig> {
use crate::image_selector::ImageSelector;
let selector = ImageSelector::new(self.config.multi_image_config.clone());
debug!("Getting config: {:?}", service_type);
selector.get_service_config(service_type).await
}
/// 获取容器网络信息(使用缓存 + 超时保护)
///
/// 🔧 优化:使用 Moka 缓存减少 Docker API 调用,使用超时保护防止阻塞
/// 📝 缓存策略:支持 None 值缓存(容器不存在或网络信息为空时)
///
/// # 返回
/// - `Ok(HashMap)`: 网络名称到 IP 地址的映射(可能为空)
/// - `Err(ConnectionError)`: 容器不存在或无法获取网络信息
pub async fn get_container_network_info(
&self,
container_id: &str,
) -> DockerResult<HashMap<String, String>> {
// 1. 尝试从缓存获取
if let Some(Some(cached)) = self.api_cache.get_network(container_id).await {
debug!("[NETWORK] getting network info: container_id={}", container_id);
// Arc::clone 只是增加引用计数,解引用后 clone HashMap
return Ok((*cached).clone());
}
// 1.5 检查是否缓存了 None空网络信息
if let Some(None) = self.api_cache.get_network(container_id).await {
debug!(
"📭 [NETWORK] Cache hit (empty network): container_id={}",
container_id
);
return Ok(HashMap::new());
}
// 2. 缓存未命中,调用 Docker API带超时
let timeout = Duration::from_secs(self.config.api_timeout_quick_seconds);
let inspect = match self.inspect_with_timeout(container_id, timeout).await {
Ok(i) => i,
Err(DockerError::BollardError(bollard::errors::Error::DockerResponseServerError {
status_code: 404,
..
})) => {
// 容器不存在 - 缓存空 HashMap
debug!(
"📭 [NETWORK] Container does not exist, caching empty network: container_id={}",
container_id
);
self.api_cache
.insert_network(container_id.to_string(), None)
.await;
return Ok(HashMap::new());
}
Err(DockerError::Timeout(_)) => {
warn!(
"[NETWORK] Query timeout, trying to get from cache: container_id={}",
container_id
);
// 超时时,尝试返回缓存中的旧值(如果有的话)
if let Some(Some(cached)) = self.api_cache.get_network(container_id).await {
return Ok((*cached).clone());
}
return Err(DockerError::Timeout(format!(
"Container network info query timeout and no available cache: container_id={}",
container_id
)));
}
Err(e) => return Err(e),
};
// 3. 解析网络信息
let mut network_ips = HashMap::new();
if let Some(network_settings) = inspect.network_settings
&& let Some(networks) = network_settings.networks
{
for (network_name, network_info) in networks {
if let Some(ip_address) = network_info.ip_address
&& !ip_address.is_empty()
{
network_ips.insert(network_name, ip_address);
}
}
}
// 4. 写入缓存(如果为空也缓存,避免重复查询)
let result_to_cache = if network_ips.is_empty() {
None
} else {
// 🔧 使用 Arc 包装,减少 clone 开销
Some(Arc::new(network_ips.clone()))
};
self.api_cache
.insert_network(container_id.to_string(), result_to_cache)
.await;
Ok(network_ips)
}
/// 检查容器健康状态
async fn check_container_health(&self, container_id: &str) -> DockerResult<()> {
use bollard::query_parameters::InspectContainerOptions;
// 检查容器详细信息
let inspect = self
.docker
.inspect_container(container_id, None::<InspectContainerOptions>)
.await
.map_err(|e| DockerError::ConnectionError(format!("failed to check container status: {}", e)))?;
// 检查容器状态
if let Some(state) = inspect.state {
let status = state.status;
let exit_code = state.exit_code.unwrap_or(-1);
match status {
Some(bollard::models::ContainerStateStatusEnum::RUNNING) => {
info!("Container {} is running", container_id);
Ok(())
}
Some(bollard::models::ContainerStateStatusEnum::EXITED) => {
let error_msg = state.error.as_deref().unwrap_or("unknown error");
error!(
"Container {} exited (exit code: {}): {}",
container_id, exit_code, error_msg
);
Err(DockerError::ContainerStartError(format!(
"Container exited immediately after startup: {} (exit code: {}), error: {}",
container_id, exit_code, error_msg
)))
}
Some(bollard::models::ContainerStateStatusEnum::CREATED) => {
warn!(
"container {} already created but not started",
container_id
);
Err(DockerError::ContainerStartError(format!(
"Container created but not started: {}",
container_id
)))
}
Some(status) => {
let status_str = format!("{:?}", status);
error!(
"Container {} has unexpected status: {}",
container_id, status_str
);
Err(DockerError::ContainerStartError(format!(
"Container in unknown state: {} - {}",
container_id, status_str
)))
}
None => {
error!("Container {} status is empty", container_id);
Err(DockerError::ContainerStartError(format!(
"Container status is empty: {}",
container_id
)))
}
}
} else {
error!(
"Unable to get container {} status",
container_id
);
Err(DockerError::ContainerStartError(format!(
"unable to get container status info: {}",
container_id
)))
}
}
/// 根据模式列出 Docker 容器
///
/// # Arguments
/// * `pattern` - 容器名称模式,支持通配符(如 "rcoder-agent-*"
///
/// # Returns
/// 返回匹配的容器信息列表
pub async fn list_containers_with_pattern(
&self,
pattern: &str,
) -> DockerResult<Vec<ContainerSummary>> {
info!("Listing containers: pattern={}", pattern);
// 🎯 获取当前容器的 ID用于排除自己
let current_container_id = std::env::var("HOSTNAME").ok();
// 使用 Docker API 列出所有容器(包括停止的)
let options = Some(ListContainersOptions {
all: true,
..Default::default()
});
let containers = self
.docker
.list_containers(options)
.await
.map_err(|e| DockerError::ConnectionError(format!("failed to get container list: {}", e)))?;
// 创建过滤器
let filter = ContainerFilter::name_pattern(pattern);
// 过滤容器,排除当前容器自己
let matched_containers: Vec<ContainerSummary> = containers
.clone()
.into_iter()
.filter(|container| {
// 排除当前容器自己
if let Some(ref current_id) = current_container_id {
if let Some(ref container_id) = container.id {
// HOSTNAME 是容器 ID 的前 12 位
if container_id.starts_with(current_id) {
info!("🚫 skip removing container: {}", container_id);
return false;
}
}
}
filter.matches(container)
})
.collect();
info!(
"Container lookup completed: total={}, matched={} (self excluded), pattern={}",
containers.len(),
matched_containers.len(),
pattern
);
Ok(matched_containers)
}
/// 批量停止并删除指定的容器
///
/// # Arguments
/// * `container_ids` - 要删除的容器ID列表
/// * `options` - 清理选项
///
/// # Returns
/// 返回清理操作结果统计
pub async fn stop_and_remove_containers_by_ids(
&self,
container_ids: Vec<String>,
options: CleanupOptions,
) -> DockerResult<CleanupResult> {
info!(
"🔥 Starting cleanup container: count={}",
container_ids.len()
);
let start_time = Instant::now();
let mut result = CleanupResult::default();
result.total_found = container_ids.len();
for container_id in &container_ids {
match self
.stop_and_remove_single_container(container_id, &options)
.await
{
Ok(_) => {
result.successfully_removed += 1;
result.removed_container_ids.push(container_id.clone());
info!("containercleanupsucceeded: {}", container_id);
}
Err(e) => {
result.failed_removals += 1;
result
.failed_removals_details
.push(ContainerRemovalFailure {
container_id: container_id.clone(),
container_name: container_id.clone(), // 我们可能不知道名称使用ID
error_message: e.to_string(),
});
error!("containercleanupfailed: {} - {}", container_id, e);
}
}
}
result.duration_ms = start_time.elapsed().as_millis() as u64;
info!(
"Batch container cleanup completed: total={}, success={}, failed={}, duration={}ms",
result.total_found,
result.successfully_removed,
result.failed_removals,
result.duration_ms
);
Ok(result)
}
/// 停止并删除单个容器
async fn stop_and_remove_single_container(
&self,
container_id: &str,
options: &CleanupOptions,
) -> DockerResult<()> {
info!("Cleaning up container: {}", container_id);
// 第一步:获取容器信息
let container_info = self.inspect_container_for_cleanup(container_id).await?;
// 第二步:检查容器状态并决定是否需要停止
match container_info
.state
.as_ref()
.and_then(|s| s.status.as_ref())
{
Some(status) if status.to_string() == "running" => {
if !options.force_remove_running {
info!(
"⚠️ Container {} is running, skip (force=false)",
container_id
);
return Ok(());
}
if options.wait_for_graceful_stop {
info!("🛑 Gracefully stopped container: {}", container_id);
if let Err(e) = self
.graceful_stop_container(container_id, options.stop_timeout_seconds)
.await
{
warn!(
"gracefulstoppedfailed, forcestopped: {} - {}",
container_id, e
);
// 强制停止
self.force_stop_container(container_id).await?;
}
} else {
// 直接强制停止
self.force_stop_container(container_id).await?;
}
}
Some(_) => {
info!("Container {} is not running", container_id);
}
None => {
warn!("Unable to get container {} status", container_id);
}
}
// 第三步:删除容器
self.remove_single_container(container_id, options.remove_associated_volumes)
.await?;
info!("containercleanupcompleted: {}", container_id);
Ok(())
}
/// 获取容器信息用于清理
async fn inspect_container_for_cleanup(
&self,
container_id: &str,
) -> Result<bollard::models::ContainerInspectResponse, DockerError> {
let options = Some(InspectContainerOptions { size: false });
self.docker
.inspect_container(container_id, options)
.await
.map_err(|e| DockerError::ConnectionError(format!("failed to get container info: {}", e)))
}
/// 优雅停止容器
async fn graceful_stop_container(
&self,
container_id: &str,
timeout_seconds: u64,
) -> DockerResult<()> {
let stop_options = Some(StopContainerOptions {
t: Some(
(timeout_seconds as i32)
.try_into()
.expect("timeout should be within valid range"),
),
signal: None::<String>,
});
self.docker
.stop_container(container_id, stop_options)
.await
.map_err(|e| DockerError::ContainerStopError(format!("failed to gracefully stop container: {}", e)))
}
/// 强制停止容器
async fn force_stop_container(&self, container_id: &str) -> DockerResult<()> {
let stop_options = Some(StopContainerOptions {
t: None::<i32>,
signal: None::<String>,
});
self.docker
.stop_container(container_id, stop_options)
.await
.map_err(|e| DockerError::ContainerStopError(format!("failed to force stop container: {}", e)))
}
/// 删除单个容器
async fn remove_single_container(
&self,
container_id: &str,
remove_volumes: bool,
) -> DockerResult<()> {
let remove_options = Some(RemoveContainerOptions {
force: true,
v: remove_volumes,
..Default::default()
});
self.docker
.remove_container(container_id, remove_options)
.await
.map_err(|e| DockerError::ContainerRemoveError(format!("failed to delete container: {}", e)))
}
/// 使用模式匹配清理容器(主要接口)
///
/// # Arguments
/// * `pattern` - 容器名称模式(如 "rcoder-agent-*"
/// * `options` - 清理选项
///
/// # Returns
/// 返回清理结果统计
pub async fn cleanup_containers_with_pattern(
&self,
pattern: &str,
options: CleanupOptions,
) -> DockerResult<CleanupResult> {
info!(
"🧹 Starting cleanup container: pattern={:?}",
pattern
);
// 第一步:查找匹配的容器
let matched_containers = self.list_containers_with_pattern(pattern).await?;
// 第二步提取容器ID
let container_ids: Vec<String> = matched_containers
.iter()
.filter_map(|container| container.id.as_ref())
.cloned()
.collect();
info!(
"Found {} matching containers: pattern={}",
container_ids.len(),
pattern
);
// 第三步:批量清理
let result = self
.stop_and_remove_containers_by_ids(container_ids, options)
.await;
// 第四步:从内部映射中移除已清理的容器
self.cleanup_internal_mappings(&matched_containers).await;
result
}
/// 从内部映射中清理已删除的容器
async fn cleanup_internal_mappings(&self, removed_containers: &[ContainerSummary]) {
for container in removed_containers {
if let Some(container_id) = &container.id {
// 从内存映射中查找并移除
// 从内存映射中查找并安全移除
for info in self.containers.list().await {
if info.container_id == *container_id {
// 使用安全移除,只有 container_id 匹配时才移除 (防止误删重启后的新容器)
if self
.containers
.remove_if_container_id(&info.project_id, container_id)
.await
.is_some()
{
info!(
"Removed from internal mapping: project_id={}, container_id={}",
info.project_id, container_id
);
}
}
}
}
}
}
/// 获取主网络名称(异步,返回动态检测的值)
pub async fn get_main_network_name(&self) -> String {
self.main_network_name.read().await.clone()
}
/// 🔍 动态检测当前主容器所在的网络名称(静态方法,用于初始化)
///
/// 通过检查当前容器(运行 DockerManager 的容器)所连接的网络来确定主网络名称
/// 这样可以适应不同的 Docker Compose project name
async fn detect_main_network_name_static(
docker: &Docker,
network_base_name: &str,
) -> DockerResult<String> {
use bollard::query_parameters::InspectContainerOptions;
// 🎯 优化:直接通过 HOSTNAME 环境变量 inspect 当前容器,无需列出所有容器
let hostname = std::env::var("HOSTNAME").map_err(|_| {
DockerError::ConnectionError(
"unable to get HOSTNAME environment variable. Please ensure code is running in Docker container.".to_string(),
)
})?;
debug!("Detecting container hostname: {}", hostname);
// 直接 inspect 当前容器hostname 通常是容器 ID 的前12位但 Docker API 支持前缀匹配)
let inspect = docker
.inspect_container(&hostname, None::<InspectContainerOptions>)
.await
.map_err(|e| {
DockerError::ConnectionError(format!(
"unable to get current container info (hostname: {}): {}",
hostname, e
))
})?;
// 获取网络配置
if let Some(network_settings) = inspect.network_settings
&& let Some(networks) = network_settings.networks
{
// 查找包含指定网络基础名称的网络
for network_name in networks.keys() {
if network_name.contains(network_base_name) {
info!("detected network: {}", network_name);
return Ok(network_name.clone());
}
}
// 如果没找到包含指定基础名称的回退到第一个可用网络Docker Compose 默认网络)
if let Some(fallback_name) = networks.keys().next() {
warn!(
"未找到包含 '{}' 的网络,回退使用当前容器的默认网络: {} (可用网络: {:?})",
network_base_name,
fallback_name,
networks.keys().collect::<Vec<_>>()
);
return Ok(fallback_name.clone());
}
return Err(DockerError::ConnectionError(
"当前容器没有任何网络配置".to_string(),
));
}
Err(DockerError::ConnectionError(format!(
"Current container (hostname: {}) has no network configuration information",
hostname
)))
}
/// 🔍 动态检测当前主容器所在的网络名称
///
/// 通过检查当前容器(运行 DockerManager 的容器)所连接的网络来确定主网络名称
/// 这样可以适应不同的 Docker Compose project name
pub async fn detect_main_network_name(&self) -> DockerResult<String> {
Self::detect_main_network_name_static(&self.docker, &self.config.network_base_name).await
}
}
impl std::fmt::Debug for DockerManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DockerManager")
.field("containers", &"ContainerStateHandle (async)")
.field("config", &self.config)
.finish()
}
}
/// 为了支持 futures Stream需要导入 StreamExt trait
use futures_util::stream::StreamExt;
#[cfg(test)]
mod tests {
use super::*;
use bollard::Docker;
/// 测试通过容器名称获取创建时间
///
/// 使用真实容器 `rcoder-rcoder-1` 验证时间戳解析
#[tokio::test]
#[ignore] // 需要本地环境有 Docker 和容器,默认忽略
#[allow(deprecated)] // 测试代码使用 deprecated API 是可接受的
async fn test_get_container_creation_time_by_name_real() {
// 直接使用 Bollard 创建 Docker 客户端
let docker = Docker::connect_with_local_defaults().expect("Failed to connect to Docker");
// 测试容器名称
let container_name = "rcoder-rcoder-1";
println!("\n🔍 checking container: {}", container_name);
println!("─────────────────────────────────────────");
// 直接调用 Docker API 获取容器信息
match docker
.inspect_container(
container_name,
None::<bollard::query_parameters::InspectContainerOptions>,
)
.await
{
Ok(details) => {
println!("✅ succeeded getcontainer");
// 获取创建时间字符串
if let Some(ref created_str) = details.created {
println!(" Docker API created: {}", created_str);
// 解析时间戳
match DateTime::parse_from_rfc3339(&created_str) {
Ok(created_time) => {
let created_time_utc = created_time.with_timezone(&Utc);
println!(" created UTC: {}", created_time_utc);
// 计算容器年龄
let age = Utc::now().signed_duration_since(created_time_utc);
println!(" container age (seconds): {}", age.num_seconds());
println!(" container age (minutes): {}", age.num_minutes());
println!(" container age (hours): {}", age.num_hours());
println!(" container age (days): {}", age.num_days());
// 验证时间是否合理
assert!(created_time_utc < Utc::now(), "创建时间应该在过去");
assert!(age.num_days() < 365, "创建时间不应该超过 1 年");
println!("\n✅ timestamp test passed!");
}
Err(e) => {
panic!("❌ RFC3339 时间戳解析失败: {}", e);
}
}
} else {
panic!("❌ 容器没有 created 字段");
}
// 使用 Docker CLI 对比验证
println!("\n🔍 checking Docker CLI:");
println!("─────────────────────────────────────────");
use std::process::Command;
let output = Command::new("docker")
.args(&["inspect", container_name, "--format", "{{.Created}}"])
.output()
.expect("Failed to run docker inspect");
let docker_cli_time = String::from_utf8_lossy(&output.stdout);
println!(" Docker CLI created: {}", docker_cli_time.trim());
// 解析 Docker CLI 返回的时间
if let Ok(docker_time) = DateTime::parse_from_rfc3339(docker_cli_time.trim()) {
let docker_time_utc = docker_time.with_timezone(&Utc);
println!(" Docker CLI UTC: {}", docker_time_utc);
// 从 Docker API 获取的时间
if let Some(ref created_str) = details.created {
if let Ok(api_time) = DateTime::parse_from_rfc3339(&created_str) {
let api_time_utc = api_time.with_timezone(&Utc);
println!(" API created UTC: {}", api_time_utc);
// 时间差应该为 0应该完全一致
let diff =
(docker_time_utc.timestamp() - api_time_utc.timestamp()).abs();
println!(" time diff: {} seconds", diff);
assert_eq!(diff, 0, "API 和 CLI 返回的时间应该完全一致");
println!("\n✅ Docker CLI check passed!");
}
}
}
}
Err(e) => {
panic!("❌ 获取容器信息失败: {}", e);
}
}
}
/// 测试 Unix 时间戳解析(验证 bug 修复)
#[tokio::test]
#[ignore]
#[allow(deprecated)] // 测试代码使用 deprecated API 是可接受的
async fn test_unix_timestamp_parsing() {
use chrono::TimeZone;
println!("\n🔍 testing Unix timestamp ( old bug )");
println!("─────────────────────────────────────────");
// 容器实际创建时间: 2026-01-19T07:35:53Z
let expected_time = Utc.with_ymd_and_hms(2026, 1, 19, 7, 35, 53).unwrap();
let unix_timestamp = expected_time.timestamp(); // 1768808153 秒
println!(" expected time: {}", expected_time);
println!(" unix timestamp: {}", unix_timestamp);
// 使用我们的解析函数
match DockerManager::parse_unix_timestamp(unix_timestamp, "test") {
Ok(parsed_time) => {
println!(" parsed time: {}", parsed_time);
let diff = (parsed_time.timestamp() - expected_time.timestamp()).abs();
println!(" time diff: {} seconds", diff);
assert_eq!(diff, 0, "时间戳解析应该完全准确");
println!("\n✅ Unix timestamp test passed!");
}
Err(e) => {
panic!("❌ 解析失败: {}", e);
}
}
// 验证旧代码的错误
println!("\n🔍 verifying bug:");
let wrong_seconds = unix_timestamp / 1000; // 旧代码的错误处理
let wrong_time = Utc.timestamp_opt(wrong_seconds, 0).single().unwrap();
println!(" wrong time: {} (error!)", wrong_time);
println!(
" 与正确时间相差: {}",
(expected_time.timestamp() - wrong_time.timestamp()) / 86400
);
}
/// 测试时间戳解析的完整流程
///
/// 主动创建一个测试容器,同时使用 list_containers 和 inspect_container API
/// 验证 parse_unix_timestamp 和 parse_rfc3339_timestamp 的正确性
#[tokio::test]
#[ignore] // 需要本地 Docker 环境
async fn test_timestamp_parsing_with_real_container() {
use bollard::models::ContainerCreateBody;
use bollard::query_parameters::{
CreateContainerOptionsBuilder, CreateImageOptionsBuilder, ListContainersOptionsBuilder,
RemoveContainerOptionsBuilder,
};
use futures_util::TryStreamExt;
// 连接 Docker
let docker = Docker::connect_with_local_defaults().expect("Failed to connect to Docker");
// 测试容器名称(使用时间戳避免冲突)
let container_name = format!("test-timestamp-{}", chrono::Utc::now().timestamp());
println!("\n🔍 testing timestamp parsing");
println!("─────────────────────────────────────────");
println!(" testing container: {}", container_name);
// 拉取 alpine 镜像(如果不存在)
println!("\n📥 pulling image: alpine:latest");
let create_image_options = CreateImageOptionsBuilder::default()
.from_image("alpine:latest")
.build();
let _ = docker
.create_image(Some(create_image_options), None, None)
.try_collect::<Vec<_>>()
.await;
// 1. 创建测试容器(使用 alpine 镜像)
let config = ContainerCreateBody {
image: Some("alpine:latest".to_string()),
cmd: Some(vec!["sleep".to_string(), "3600".to_string()]),
host_config: Some(bollard::models::HostConfig {
auto_remove: Some(false),
..Default::default()
}),
..Default::default()
};
let create_options = CreateContainerOptionsBuilder::default()
.name(&container_name)
.build();
let create_result = docker
.create_container(Some(create_options), config)
.await
.expect("Failed to create test container");
println!("✅ container already created: {}", create_result.id);
// 2. 启动容器
docker
.start_container(
&container_name,
None::<bollard::query_parameters::StartContainerOptions>,
)
.await
.expect("Failed to start test container");
println!("✅ container already started");
// 等待容器完全启动
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// 3. 使用 list_containers API 获取 Unix 时间戳
println!("\n📋 testing list_containers API (Unix timestamp):");
println!("─────────────────────────────────────────");
let mut filters = std::collections::HashMap::new();
filters.insert("name".to_string(), vec![container_name.clone()]);
let list_options = ListContainersOptionsBuilder::default()
.all(true)
.filters(&filters)
.build();
let containers = docker
.list_containers(Some(list_options))
.await
.expect("Failed to list containers");
assert_eq!(containers.len(), 1, "应该只找到一个测试容器");
let container = &containers[0];
let unix_timestamp = container.created.expect("容器应该有 created 字段");
println!(" unix timestamp: {} seconds", unix_timestamp);
// 使用 parse_unix_timestamp 解析
let parsed_unix_time = DockerManager::parse_unix_timestamp(
unix_timestamp,
&format!("container {}", container_name),
)
.expect("parse_unix_timestamp 应该成功");
println!(" parsed (UTC): {}", parsed_unix_time);
// 4. 使用 inspect_container API 获取 RFC3339 时间戳
println!("\n📋 testing inspect_container API (RFC3339 timestamp):");
println!("─────────────────────────────────────────");
let details = docker
.inspect_container(&container_name, None::<InspectContainerOptions>)
.await
.expect("Failed to inspect container");
let rfc3339_str = details.created.expect("容器应该有 created 字段");
println!(" RFC3339 timestamp: {}", rfc3339_str);
// 使用 parse_rfc3339_timestamp 解析
let parsed_rfc3339_time = DockerManager::parse_rfc3339_timestamp(
&rfc3339_str,
&format!("container {}", container_name),
)
.expect("parse_rfc3339_timestamp 应该成功");
println!(" parsed (UTC): {}", parsed_rfc3339_time);
// 5. 验证两个解析结果的一致性
println!("\n🔍 comparing API results:");
println!("─────────────────────────────────────────");
let time_diff = (parsed_unix_time.timestamp() - parsed_rfc3339_time.timestamp()).abs();
println!(" list_containers parsed: {}", parsed_unix_time);
println!(" inspect_container parsed: {}", parsed_rfc3339_time);
println!(" time diff: {} seconds", time_diff);
// 两个 API 应该返回相同的时间(允许 1 秒误差,因为精度不同)
assert!(
time_diff <= 1,
"两个 API 的时间差应该在 1 秒以内,实际差异: {} 秒",
time_diff
);
// 6. 验证时间合理性
println!("\n🔍 verifying timestamps:");
println!("─────────────────────────────────────────");
let now = Utc::now();
let age = now.signed_duration_since(parsed_unix_time);
println!(" current time: {}", now);
println!(" container age (seconds): {}", age.num_seconds());
assert!(age.num_seconds() >= 0, "容器创建时间应该在过去");
assert!(age.num_seconds() < 60, "容器应该是刚创建的(< 60 秒)");
println!("\n✅ timestamp test passed!");
// 7. 清理测试容器
println!("\n🧹 cleaning up test container...");
let remove_options = RemoveContainerOptionsBuilder::default().force(true).build();
docker
.remove_container(&container_name, Some(remove_options))
.await
.expect("Failed to cleanup test container");
println!("✅ container already cleaned up: {}", container_name);
}
}