添加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,61 @@
[package]
name = "docker_manager"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
description.workspace = true
publish = false
[dependencies]
# Docker API
bollard = { workspace = true }
# Async runtime
tokio = { workspace = true }
futures-util = { workspace = true }
# HTTP client (for health checks)
reqwest = { workspace = true }
# Serialization
serde = { workspace = true }
serde_json = { workspace = true }
# Error handling
anyhow = { workspace = true }
thiserror = { workspace = true }
# Logging
tracing = { workspace = true }
# UUID
uuid = { workspace = true }
# Date/time
chrono = { workspace = true }
# Utilities
dashmap = { workspace = true }
moka = { version = "0.12", features = ["future"] }
# Shared types
shared_types = { path = "../shared_types" }
# Runtime API (always needed)
container-runtime-api = { path = "../container-runtime-api" }
# Async runtime
async-trait = { workspace = true }
# Kubernetes runtime support (feature-gated)
kube = { version = "0.98", features = ["runtime", "client", "kube-runtime", "config"], optional = true }
k8s-openapi = { version = "0.24", features = ["v1_30"], optional = true }
[features]
default = []
# Kubernetes runtime support
kubernetes = ["kube", "k8s-openapi"]
# 🔧 eBPF 调试模式:启用容器特权,允许使用 eBPF 诊断工具
# 警告:仅在开发调试时使用,生产环境请勿启用
ebpf-debug = []

View File

@@ -0,0 +1,234 @@
# Docker Manager
基于 bollard 库的 Docker 容器动态管理模块,用于 RCoder 项目中的 Docker Agent 管理。
## 功能特性
- ✅ 动态创建和销毁 Docker 容器
- ✅ 支持项目工作目录挂载
- ✅ 容器状态监控和日志获取
- ✅ 支持环境变量和端口映射配置
- ✅ 自动镜像拉取和资源限制
- ✅ 容器生命周期管理
## 核心组件
### 1. DockerManager
主要的 Docker 管理器,提供容器的完整生命周期管理。
```rust
use docker_manager::{DockerManager, DockerManagerConfig};
// 创建 Docker 管理器
let config = DockerManagerConfig::default();
let docker_manager = DockerManager::new(config).await?;
// 创建容器
let container_info = docker_manager.create_container(config).await?;
// 停止容器
docker_manager.stop_container("project_id").await?;
```
### 2. DockerContainerConfig
容器配置结构体,定义容器的各种参数。
```rust
use docker_manager::DockerContainerConfig;
let config = DockerContainerConfig {
project_id: "my_project".to_string(),
image: "registry.yichamao.com/rcoder:latest".to_string(),
host_path: "/path/to/project".to_string(),
container_path: "/app/workspace".to_string(),
env_vars: env_map,
port_bindings: port_map,
..Default::default()
};
```
### 3. DockerUtils
工具函数集合,简化常见操作。
```rust
use docker_manager::DockerUtils;
// 根据项目ID创建配置
let config = DockerUtils::create_config_from_project_id(
"project_123",
"./project_workspace",
Some("custom:image".to_string()),
);
```
## 使用场景
### 1. RCoder Docker Agent
在 RCoder 项目中Docker Agent 用于为每个项目创建独立的运行环境:
```rust
use docker_manager::{DockerAgentManager, DockerUtils};
use rcoder::model::{AgentType, ChatPrompt};
// 创建 Docker Agent 管理器
let docker_agent_manager = DockerAgentManager::new().await?;
// 为项目创建 Docker Agent
let chat_prompt = ChatPrompt {
project_id: "project_123".to_string(),
agent_type: AgentType::Docker,
// ... 其他字段
};
let docker_agent = docker_agent_manager.create_docker_agent(
&chat_prompt,
AgentType::Docker
).await?;
```
### 2. 项目隔离
每个项目在独立的 Docker 容器中运行,确保环境隔离:
- 挂载路径: `./project_workspace/{project_id}``/app/workspace`
- 镜像: `registry.yichamao.com/rcoder:latest`
- 网络: 使用 host 模式以获得更好的性能
### 3. 环境变量配置
Docker Agent 支持丰富的环境变量配置:
```rust
let mut env_vars = HashMap::new();
env_vars.insert("ANTHROPIC_AUTH_TOKEN".to_string(), "your_token".to_string());
env_vars.insert("PROJECT_ID".to_string(), "project_123".to_string());
env_vars.insert("DOCKER_AGENT_TYPE".to_string(), "claude".to_string());
```
## 配置选项
### DockerManagerConfig
```rust
pub struct DockerManagerConfig {
pub docker_host: Option<String>, // Docker 守护进程地址
pub default_image: String, // 默认镜像
pub default_network_mode: String, // 默认网络模式
pub default_work_dir: String, // 默认工作目录
pub auto_cleanup: bool, // 是否启用自动清理
pub container_ttl_seconds: Option<u64>, // 容器存活时间
}
```
### 环境变量配置
可以通过环境变量配置 Docker 管理器:
- `DOCKER_HOST`: Docker 守护进程地址
- `DEFAULT_DOCKER_IMAGE`: 默认镜像
- `DOCKER_NETWORK_MODE`: 网络模式
- `DOCKER_WORK_DIR`: 工作目录
- `DOCKER_AUTO_CLEANUP`: 自动清理
- `DOCKER_CONTAINER_TTL`: 容器TTL
## 集成到 RCoder
### 1. 启用 Docker Agent
设置环境变量启用 Docker Agent
```bash
export USE_DOCKER_AGENT=true
```
### 2. 项目目录结构
```
./project_workspace/
├── project_123/ # 项目 123 的工作目录
│ ├── src/
│ ├── Cargo.toml
│ └── ...
├── project_456/ # 项目 456 的工作目录
│ └── ...
```
### 3. Docker 镜像
使用预构建的 RCoder Docker 镜像:
- 镜像地址: `registry.yichamao.com/rcoder:latest`
- 包含完整的 AI 开发工具链
- 支持 Claude Code 和 Codex Agent
## 错误处理
所有操作都返回 `DockerResult<T>`,包含详细的错误信息:
```rust
match docker_manager.create_container(config).await {
Ok(container_info) => {
println!("容器创建成功: {}", container_info.container_name);
}
Err(DockerError::ContainerCreationError(msg)) => {
eprintln!("容器创建失败: {}", msg);
}
Err(e) => {
eprintln!("其他错误: {}", e);
}
}
```
## 日志和监控
### 获取容器日志
```rust
// 获取最后 50 行日志
let logs = docker_manager.get_container_logs("project_id", 50).await?;
println!("容器日志:\n{}", logs);
```
### 监控容器状态
```rust
// 检查容器状态
let status = docker_manager.update_container_status("project_id").await?;
if let Some(status) = status {
println!("容器状态: {:?}", status);
}
```
## 最佳实践
1. **资源管理**: 及时停止不用的容器以释放资源
2. **错误处理**: 始终检查操作结果并处理错误
3. **日志监控**: 定期检查容器日志以了解运行状态
4. **环境隔离**: 为每个项目使用独立的容器
5. **镜像管理**: 使用固定版本的镜像以避免意外更新
## 故障排除
### 常见问题
1. **Docker 连接失败**
- 检查 Docker 守护进程是否运行
- 验证 Docker socket 权限
2. **镜像拉取失败**
- 检查网络连接
- 验证镜像地址和认证信息
3. **容器启动失败**
- 检查资源限制内存、CPU
- 验证挂载路径权限
- 查看容器日志了解详细错误
### 调试模式
启用详细日志进行调试:
```bash
export RUST_LOG=debug
cargo run --example basic_usage
```

View File

@@ -0,0 +1,350 @@
//! 容器配置构建器
//!
//! 使用 Builder 模式构建 DockerContainerConfig
use crate::{DockerContainerConfig, DockerResult, MountPoint, ResourceLimits};
use std::collections::HashMap;
use tracing::debug;
/// 容器配置构建器
///
/// 使用 Builder 模式提供灵活的容器配置构建接口
///
/// # Examples
/// ```no_run
/// use docker_manager::container_builder::ContainerConfigBuilder;
///
/// # async fn example() -> docker_manager::DockerResult<()> {
/// let config = ContainerConfigBuilder::new("project-123")
/// .image("registry.example.com/agent-runner:latest")
/// .host_path("/host/path")
/// .container_path("/app/project_workspace/project-123")
/// .env("PROJECT_ID", "project-123")
/// .network_name("rcoder_agent-network")
/// .build()?;
/// # Ok(())
/// # }
/// ```
pub struct ContainerConfigBuilder {
project_id: String,
image: Option<String>,
name_prefix: Option<String>,
host_path: Option<String>,
container_path: Option<String>,
work_dir: Option<String>,
env_vars: HashMap<String, String>,
port_bindings: HashMap<String, String>,
network_mode: Option<String>,
auto_remove: bool,
resource_limits: Option<ResourceLimits>,
extra_mounts: Vec<MountPoint>,
command: Option<Vec<String>>,
entrypoint: Option<Vec<String>>,
network_name: Option<String>,
// 新增字段(隔离类型支持)
pod_id: Option<String>,
tenant_id: Option<String>,
space_id: Option<String>,
isolation_type: Option<String>,
}
impl ContainerConfigBuilder {
/// 创建新的容器配置构建器
///
/// # Arguments
/// * `project_id` - 项目ID必需
pub fn new(project_id: impl Into<String>) -> Self {
Self {
project_id: project_id.into(),
image: None,
name_prefix: None,
host_path: None,
container_path: None,
work_dir: None,
env_vars: HashMap::new(),
port_bindings: HashMap::new(),
network_mode: None,
auto_remove: false,
resource_limits: None,
extra_mounts: Vec::new(),
command: None,
entrypoint: None,
network_name: None,
// 新增字段(隔离类型支持)
pod_id: None,
tenant_id: None,
space_id: None,
isolation_type: None,
}
}
/// 设置 Docker 镜像
pub fn image(mut self, image: impl Into<String>) -> Self {
self.image = Some(image.into());
self
}
/// 设置容器名称前缀
pub fn name_prefix(mut self, prefix: impl Into<String>) -> Self {
self.name_prefix = Some(prefix.into());
self
}
/// 设置宿主机路径
pub fn host_path(mut self, path: impl Into<String>) -> Self {
self.host_path = Some(path.into());
self
}
/// 设置容器内路径
pub fn container_path(mut self, path: impl Into<String>) -> Self {
self.container_path = Some(path.into());
self
}
/// 设置工作目录
pub fn work_dir(mut self, dir: impl Into<String>) -> Self {
self.work_dir = Some(dir.into());
self
}
/// 添加单个环境变量
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env_vars.insert(key.into(), value.into());
self
}
/// 批量添加环境变量
pub fn envs(mut self, vars: HashMap<String, String>) -> Self {
self.env_vars.extend(vars);
self
}
/// 添加端口映射
pub fn port_binding(
mut self,
container_port: impl Into<String>,
host_port: impl Into<String>,
) -> Self {
self.port_bindings
.insert(container_port.into(), host_port.into());
self
}
/// 批量添加端口映射
pub fn port_bindings(mut self, bindings: HashMap<String, String>) -> Self {
self.port_bindings.extend(bindings);
self
}
/// 设置网络模式
pub fn network_mode(mut self, mode: impl Into<String>) -> Self {
self.network_mode = Some(mode.into());
self
}
/// 设置自动删除标志
pub fn auto_remove(mut self, enabled: bool) -> Self {
self.auto_remove = enabled;
self
}
/// 设置资源限制
pub fn resource_limits(mut self, limits: ResourceLimits) -> Self {
self.resource_limits = Some(limits);
self
}
/// 添加单个挂载点
pub fn add_mount(mut self, mount: MountPoint) -> Self {
self.extra_mounts.push(mount);
self
}
/// 批量添加挂载点
pub fn add_mounts(mut self, mounts: Vec<MountPoint>) -> Self {
self.extra_mounts.extend(mounts);
self
}
/// 设置启动命令
pub fn command(mut self, command: Vec<String>) -> Self {
self.command = Some(command);
self
}
/// 设置入口点
pub fn entrypoint(mut self, entrypoint: Vec<String>) -> Self {
self.entrypoint = Some(entrypoint);
self
}
/// 设置网络名称
pub fn network_name(mut self, name: impl Into<String>) -> Self {
self.network_name = Some(name.into());
self
}
/// 设置 pod_id容器唯一标识用于容器复用
pub fn pod_id(mut self, pod_id: impl Into<String>) -> Self {
self.pod_id = Some(pod_id.into());
self
}
/// 设置 tenant_id租户 ID
pub fn tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
self.tenant_id = Some(tenant_id.into());
self
}
/// 设置 space_id空间 ID
pub fn space_id(mut self, space_id: impl Into<String>) -> Self {
self.space_id = Some(space_id.into());
self
}
/// 设置 isolation_type隔离类型
pub fn isolation_type(mut self, isolation_type: impl Into<String>) -> Self {
self.isolation_type = Some(isolation_type.into());
self
}
/// 构建 DockerContainerConfig
///
/// # Returns
/// * `DockerResult<DockerContainerConfig>` - 构建的配置或错误
pub fn build(self) -> DockerResult<DockerContainerConfig> {
debug!("builtcontainerconfig, projectID: {}", self.project_id);
// 使用默认值或提供的值
let image = self.image.unwrap_or_else(crate::default_docker_image);
let name_prefix = self
.name_prefix
.unwrap_or_else(|| "rcoder-agent".to_string());
let host_path = self.host_path.unwrap_or_default();
let container_path = self
.container_path
.unwrap_or_else(|| crate::DEFAULT_WORK_DIR.to_string());
let work_dir = self
.work_dir
.unwrap_or_else(|| crate::DEFAULT_WORK_DIR.to_string());
let network_mode = self
.network_mode
.unwrap_or_else(|| crate::DEFAULT_NETWORK_MODE.to_string());
let config = DockerContainerConfig {
project_id: self.project_id,
image,
name_prefix,
host_path,
container_path,
work_dir,
env_vars: self.env_vars,
port_bindings: self.port_bindings,
network_mode,
auto_remove: self.auto_remove,
resource_limits: self.resource_limits,
extra_mounts: self.extra_mounts,
command: self.command,
entrypoint: self.entrypoint,
network_name: self.network_name,
// 新增字段(隔离类型支持)
pod_id: self.pod_id,
tenant_id: self.tenant_id,
space_id: self.space_id,
isolation_type: self.isolation_type,
};
debug!(
"Container config built: image={}, network={:?}, mounts={}",
config.image,
config.network_name,
config.extra_mounts.len()
);
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_minimal() {
let config = ContainerConfigBuilder::new("test-project").build().unwrap();
assert_eq!(config.project_id, "test-project");
assert_eq!(config.name_prefix, "rcoder-agent");
assert!(!config.auto_remove);
}
#[test]
fn test_builder_full() {
let config = ContainerConfigBuilder::new("test-project")
.image("custom-image:latest")
.name_prefix("custom-prefix")
.host_path("/host/path")
.container_path("/container/path")
.work_dir("/work")
.env("KEY1", "value1")
.env("KEY2", "value2")
.port_binding("8080", "8080")
.network_mode("bridge")
.auto_remove(true)
.network_name("test-network")
.build()
.unwrap();
assert_eq!(config.project_id, "test-project");
assert_eq!(config.image, "custom-image:latest");
assert_eq!(config.name_prefix, "custom-prefix");
assert_eq!(config.host_path, "/host/path");
assert_eq!(config.container_path, "/container/path");
assert_eq!(config.work_dir, "/work");
assert_eq!(config.env_vars.len(), 2);
assert_eq!(config.port_bindings.len(), 1);
assert_eq!(config.network_mode, "bridge");
assert!(config.auto_remove);
assert_eq!(config.network_name, Some("test-network".to_string()));
}
#[test]
fn test_builder_with_mounts() {
let mount = MountPoint {
host_path: "/host/mount".to_string(),
container_path: "/container/mount".to_string(),
read_only: false,
};
let config = ContainerConfigBuilder::new("test-project")
.add_mount(mount)
.build()
.unwrap();
assert_eq!(config.extra_mounts.len(), 1);
assert_eq!(config.extra_mounts[0].host_path, "/host/mount");
}
#[test]
fn test_builder_with_resource_limits() {
let limits = ResourceLimits {
memory_limit: Some((512 * 1024 * 1024) as f64), // 512MB
cpu_limit: Some(1.0),
swap_limit: None,
};
let config = ContainerConfigBuilder::new("test-project")
.resource_limits(limits)
.build()
.unwrap();
assert!(config.resource_limits.is_some());
let resource_limits = config.resource_limits.unwrap();
assert_eq!(
resource_limits.memory_limit,
Some((512 * 1024 * 1024) as f64)
);
assert_eq!(resource_limits.cpu_limit, Some(1.0));
}
}

View File

@@ -0,0 +1,9 @@
//! 容器构建器模块
//!
//! 提供 Builder 模式的容器配置构建和挂载点处理功能
pub mod config_builder;
pub mod mount_processor;
pub use config_builder::*;
pub use mount_processor::*;

View File

@@ -0,0 +1,222 @@
//! 挂载点处理器
//!
//! 处理容器挂载点的路径解析和变量替换
use crate::path::HostPathResolver;
use crate::{DockerResult, MountPoint};
use std::collections::HashMap;
use std::path::Path;
use tracing::{debug, info};
/// 挂载点处理器
///
/// 提供挂载点路径解析、变量替换等功能
pub struct MountProcessor {
resolver: HostPathResolver,
}
impl MountProcessor {
/// 创建新的挂载点处理器
///
/// # Arguments
/// * `resolver` - 路径解析器
pub fn new(resolver: HostPathResolver) -> Self {
Self { resolver }
}
/// 创建新的挂载点处理器(异步,自动创建路径解析器)
///
/// # Returns
/// * `DockerResult<Self>` - 挂载点处理器或错误
pub async fn new_async() -> DockerResult<Self> {
let resolver = HostPathResolver::new().await?;
Ok(Self { resolver })
}
/// 使用指定的 Docker socket 创建挂载点处理器
///
/// # Arguments
/// * `docker_socket_path` - Docker socket 路径
///
/// # Returns
/// * `DockerResult<Self>` - 挂载点处理器或错误
pub async fn new_with_docker_socket(docker_socket_path: Option<String>) -> DockerResult<Self> {
let resolver = HostPathResolver::new_with_docker_socket(docker_socket_path).await?;
Ok(Self { resolver })
}
/// 处理单个挂载点
///
/// # Arguments
/// * `container_path` - 容器内路径
/// * `host_path` - 宿主机路径(可能包含变量或相对路径)
/// * `read_only` - 是否只读
/// * `variables` - 变量映射表(可选)
///
/// # Returns
/// * `DockerResult<MountPoint>` - 处理后的挂载点或错误
pub fn process_mount(
&self,
container_path: impl AsRef<str>,
host_path: impl AsRef<str>,
read_only: bool,
variables: Option<&HashMap<String, String>>,
) -> DockerResult<MountPoint> {
let container_path = container_path.as_ref();
let mut host_path = host_path.as_ref().to_string();
debug!(
"Processing mount point: {} -> {} (read_only: {})",
container_path, host_path, read_only
);
// 变量替换
if let Some(vars) = variables {
for (key, value) in vars {
let pattern = format!("{{{}}}", key);
if host_path.contains(&pattern) {
host_path = host_path.replace(&pattern, value);
debug!(" substituted: {} -> {}", pattern, value);
}
}
}
// 路径解析
let normalized_host_path = self.resolve_path(&host_path)?;
info!(
"Mount point processing completed: {} -> {}",
container_path, normalized_host_path
);
Ok(MountPoint {
container_path: container_path.to_string(),
host_path: normalized_host_path,
read_only,
})
}
/// 批量处理挂载点
///
/// # Arguments
/// * `mounts` - 挂载点列表 (container_path, host_path, read_only)
/// * `variables` - 变量映射表(可选)
///
/// # Returns
/// * `DockerResult<Vec<MountPoint>>` - 处理后的挂载点列表或错误
pub fn process_mounts(
&self,
mounts: Vec<(String, String, bool)>,
variables: Option<&HashMap<String, String>>,
) -> DockerResult<Vec<MountPoint>> {
debug!("Processing {} mounts", mounts.len());
let processed: DockerResult<Vec<MountPoint>> = mounts
.into_iter()
.map(|(container_path, host_path, read_only)| {
self.process_mount(&container_path, &host_path, read_only, variables)
})
.collect();
let processed = processed?;
info!(
"Mount processing completed: {} mounts",
processed.len()
);
Ok(processed)
}
/// 解析路径(处理相对路径和容器内路径)
///
/// # Arguments
/// * `path` - 待解析的路径
///
/// # Returns
/// * `DockerResult<String>` - 解析后的宿主机绝对路径
fn resolve_path(&self, path: &str) -> DockerResult<String> {
let path_obj = Path::new(path);
// 处理相对路径:转换为容器内绝对路径
let container_absolute_path = if path_obj.is_relative() {
let current_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("/app"));
current_dir.join(path_obj)
} else {
path_obj.to_path_buf()
};
// 检查是否为容器内路径
if container_absolute_path.starts_with("/app") {
// 容器内路径:转换为宿主机路径
debug!(
"Detected container path, resolving to host path: {}",
container_absolute_path.display()
);
let host_abs_path = self
.resolver
.resolve_to_host_path(&container_absolute_path)?;
Ok(host_abs_path.to_string_lossy().to_string())
} else {
// 可能已经是宿主机路径,直接使用
debug!(
"Using potential host path: {}",
container_absolute_path.display()
);
Ok(container_absolute_path.to_string_lossy().to_string())
}
}
/// 获取路径解析器的引用
pub fn resolver(&self) -> &HostPathResolver {
&self.resolver
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mount_processor_variable_substitution() {
// 注意:此测试需要在容器环境中运行才能完整验证路径解析
// 这里仅测试变量替换逻辑
let mut variables = HashMap::new();
variables.insert("project_id".to_string(), "test-123".to_string());
let host_path = "/path/to/{project_id}/data";
let mut result = host_path.to_string();
for (key, value) in &variables {
let pattern = format!("{{{}}}", key);
result = result.replace(&pattern, value);
}
assert_eq!(result, "/path/to/test-123/data");
}
#[test]
fn test_mount_point_structure() {
let mount = MountPoint {
container_path: "/app/data".to_string(),
host_path: "/host/data".to_string(),
read_only: false,
};
assert_eq!(mount.container_path, "/app/data");
assert_eq!(mount.host_path, "/host/data");
assert!(!mount.read_only);
}
// 注意:以下测试需要在 Docker 容器环境中运行
#[tokio::test]
#[ignore]
async fn test_mount_processor_creation() {
// 此测试仅在容器内有效
if std::env::var("HOSTNAME").is_ok() {
let result = MountProcessor::new_async().await;
assert!(result.is_ok());
}
}
}

View File

@@ -0,0 +1,302 @@
//! 容器自检测器
//!
//! 用于在容器内部通过 Docker API 检测自己的挂载信息,
//! 获取容器内路径对应的宿主机绝对路径
use anyhow::{Context, Result, anyhow, bail};
use bollard::{API_DEFAULT_VERSION, Docker};
use tokio::fs;
use tracing::{debug, info, warn};
/// 容器自检测器
///
/// 用于检测当前容器的挂载信息,获取容器内路径对应的宿主机路径
pub struct ContainerSelfInspector {
/// Docker 客户端
docker_client: Docker,
/// 当前容器ID
container_id: String,
}
impl ContainerSelfInspector {
/// 创建新的容器自检测器
///
/// # Arguments
/// * `docker_socket_path` - Docker socket 路径
///
/// # Returns
/// * `Result<Self>` - 检测器实例或错误
///
/// # Examples
/// ```rust,no_run
/// use docker_manager::ContainerSelfInspector;
///
/// # async fn example() -> anyhow::Result<()> {
/// let inspector = ContainerSelfInspector::new("/var/run/docker.sock").await?;
/// # Ok(())
/// # }
/// ```
pub async fn new(docker_socket_path: &str) -> Result<Self> {
info!(
"Initializing container self inspector, Docker socket: {}",
docker_socket_path
);
// 创建 Docker 客户端
let docker_client =
Docker::connect_with_socket(docker_socket_path, 120, API_DEFAULT_VERSION)
.context("Failed to connect to Docker socket")?;
// 测试 Docker 连接
docker_client
.ping()
.await
.context("Failed to test Docker connection, please check socket path and permissions")?;
info!("Docker connectionsucceeded");
// 获取当前容器ID
let container_id = Self::get_current_container_id()
.await
.context("Failed to get current container ID")?;
info!("Detected container ID: {}", container_id);
Ok(Self {
docker_client,
container_id,
})
}
/// 检测容器内路径对应的宿主机路径
///
/// # Arguments
/// * `container_path` - 容器内路径(如 "/app/project_workspace"
///
/// # Returns
/// * `Result<String>` - 宿主机绝对路径或错误
///
/// # Examples
/// ```rust,no_run
/// use docker_manager::ContainerSelfInspector;
///
/// # async fn example() -> anyhow::Result<()> {
/// # let inspector = ContainerSelfInspector::new("/var/run/docker.sock").await?;
/// let host_path = inspector.detect_host_path_for_container_dir("/app/project_workspace").await?;
/// println!(" message path: {:?}", host_path);
/// # Ok(())
/// # }
/// ```
pub async fn detect_host_path_for_container_dir(&self, container_path: &str) -> Result<String> {
info!("Detecting path {} host path", container_path);
// 获取容器详细信息
let inspect_result = self
.docker_client
.inspect_container(
&self.container_id,
None::<bollard::query_parameters::InspectContainerOptions>,
)
.await
.context("Failed to call Docker inspect API")?;
debug!(
"Container inspect result: {:?}",
serde_json::to_string_pretty(&inspect_result)?
);
// 解析挂载信息
if let Some(mounts) = inspect_result.mounts {
debug!("Container has {} mounts", mounts.len());
for (index, mount) in mounts.iter().enumerate() {
let mount_destination = mount
.destination
.as_ref()
.ok_or_else(|| anyhow!("mount {} has no destination field", index))?
.clone();
debug!(
"Mount point {}: {} -> {}",
index,
mount_destination,
mount.source.as_ref().unwrap_or(&String::new()).clone()
);
// 检查是否是我们要找的路径
if mount_destination == container_path {
let host_path = mount
.source
.as_ref()
.ok_or_else(|| anyhow!("mount {} has no source field", index))?
.clone();
info!(
" mount: {} -> {}",
container_path, host_path
);
return Ok(host_path);
}
}
// 如果没找到,列出所有挂载点供调试
warn!(
"not found path {} in mount, mount info:",
container_path
);
for (index, mount) in mounts.iter().enumerate() {
if let (Some(dest), Some(source)) = (&mount.destination, &mount.source) {
warn!(" {}: {} -> {}", index, dest, source);
}
}
bail!("mount info for path {} not found", container_path);
} else {
bail!("container has no mount info (mounts field is empty)");
}
}
/// 获取当前容器ID
///
/// 通过读取 `/proc/self/cgroup` 文件解析容器ID
///
/// # Returns
/// * `Result<String>` - 容器ID或错误
async fn get_current_container_id() -> Result<String> {
debug!("starting get containerID");
let cgroup_content = fs::read_to_string("/proc/self/cgroup")
.await
.with_context(|| "Failed to read /proc/self/cgroup file")?;
debug!("cgroup file: {}", cgroup_content);
// 解析 cgroup 文件获取容器ID
// 格式示例: 12:perf_event:/docker/abc123def456...
for line in cgroup_content.lines() {
debug!(" cgroup line: {}", line);
let parts: Vec<&str> = line.split(':').collect();
if parts.len() >= 3 {
let cgroup_path = parts[2];
// 检查是否是 Docker 容器
if cgroup_path.contains("/docker/") || cgroup_path.contains(".scope") {
debug!("Found Docker cgroup: {}", cgroup_path);
// 提取容器ID
let container_id = if cgroup_path.contains("/docker/") {
// 格式: /docker/abc123def456...
let id_parts: Vec<&str> = cgroup_path.split('/').collect();
if id_parts.len() >= 3 {
id_parts[2].to_string()
} else {
continue;
}
} else if cgroup_path.contains(".scope") {
// 格式: /system.slice/docker-abc123def456...scope
let scope_name = cgroup_path.split('/').next_back().unwrap_or("");
if scope_name.starts_with("docker-") && scope_name.ends_with(".scope") {
// 移除 "docker-" 前缀和 ".scope" 后缀
let id = &scope_name[7..scope_name.len() - 6];
id.to_string()
} else {
continue;
}
} else {
continue;
};
// 验证容器ID格式应该是64个字符的十六进制字符串
if container_id.len() == 64
&& container_id.chars().all(|c| c.is_ascii_hexdigit())
{
info!("Detected container ID: {}", container_id);
return Ok(container_id);
} else {
debug!("Skipping container ID: {}", container_id);
}
}
}
}
// 如果 cgroup 方法失败,尝试其他方法
warn!("Unable to get container ID from cgroup");
// 方法2尝试读取 /proc/1/cgroup主进程
if let Ok(cgroup_content) = fs::read_to_string("/proc/1/cgroup").await {
debug!(" reading /proc/1/cgroup");
for line in cgroup_content.lines() {
if line.contains("/docker/") || line.contains(".scope") {
debug!(" /proc/1/cgroup line: {}", line);
// 类似的解析逻辑...
}
}
}
// 方法3尝试读取主机名某些环境容器ID会作为主机名
if let Ok(hostname) = std::env::var("HOSTNAME") {
debug!("check HOSTNAME: {}", hostname);
if hostname.len() == 12 && hostname.chars().all(|c| c.is_ascii_hexdigit()) {
// 可能是短格式的容器ID前12位
info!(
" HOSTNAME get containerID: {}",
hostname
);
return Ok(hostname);
}
}
bail!("unable to get current container ID, please ensure container has sufficient permissions to access /proc/self/cgroup");
}
/// 验证 Docker socket 连接
///
/// # Returns
/// * `Result<()>` - 连接成功或错误
pub async fn verify_docker_connection(&self) -> Result<()> {
self.docker_client
.ping()
.await
.context("Docker socket connection test failed")?;
info!("Docker socket connection succeeded");
Ok(())
}
/// 获取容器所有挂载点信息(用于调试)
///
/// # Returns
/// * `Result<Vec<(String, String)>>` - 挂载点列表(容器路径 -> 宿主机路径)
pub async fn get_all_mounts(&self) -> Result<Vec<(String, String)>> {
let inspect_result = self
.docker_client
.inspect_container(
&self.container_id,
None::<bollard::query_parameters::InspectContainerOptions>,
)
.await
.context("Failed to call Docker inspect API")?;
let mut mounts = Vec::new();
if let Some(mount_infos) = inspect_result.mounts {
for mount in mount_infos {
if let (Some(dest), Some(source)) = (&mount.destination, &mount.source) {
mounts.push((dest.clone(), source.clone()));
}
}
}
Ok(mounts)
}
}
#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_container_id_parsing() {
// 这里应该模拟 cgroup 文件内容进行测试
// 由于测试环境不在容器内,这个测试可能需要跳过
}
}

View File

@@ -0,0 +1,364 @@
//! Container State Actor
//!
//! 使用 Actor 模式管理容器状态,避免 DashMap 跨 await 持有锁导致的死锁问题。
//!
//! # 架构
//! - `ContainerStateActor`: 独占 HashMap在独立 task 中运行,处理所有状态操作
//! - `ContainerStateHandle`: 可克隆的句柄,提供 async 方法与 Actor 通信
//!
//! # 优点
//! - 完全无锁,不会死锁
//! - 状态变更顺序化,更易调试
//! - 符合 Rust async 最佳实践
use crate::DockerContainerInfo;
use std::collections::HashMap;
use tokio::sync::{mpsc, oneshot};
use tracing::{debug, error, warn};
/// Actor 通道缓冲区大小
const CHANNEL_BUFFER_SIZE: usize = 256;
/// 容器状态操作命令
#[derive(Debug)]
pub enum ContainerStateCommand {
/// 获取容器信息
Get {
key: String,
reply: oneshot::Sender<Option<DockerContainerInfo>>,
},
/// 插入/更新容器信息
Insert {
key: String,
info: DockerContainerInfo,
},
/// 移除容器信息
Remove {
key: String,
reply: oneshot::Sender<Option<DockerContainerInfo>>,
},
/// 获取所有容器列表
List {
reply: oneshot::Sender<Vec<DockerContainerInfo>>,
},
/// 获取所有 key 列表
Keys { reply: oneshot::Sender<Vec<String>> },
/// 获取容器数量
Len { reply: oneshot::Sender<usize> },
/// 检查 key 是否存在
Contains {
key: String,
reply: oneshot::Sender<bool>,
},
/// 通过回调更新容器信息(用于原地更新)
UpdateWith {
key: String,
/// 更新后的信息(如果 key 存在)
updated_info: DockerContainerInfo,
reply: oneshot::Sender<bool>,
},
/// 条件移除:只有当 container_id 匹配时才移除
RemoveIfContainerId {
key: String,
container_id: String,
reply: oneshot::Sender<Option<DockerContainerInfo>>,
},
}
/// 容器状态 Actor
///
/// 独占 HashMap在独立 task 中运行
pub struct ContainerStateActor {
containers: HashMap<String, DockerContainerInfo>,
receiver: mpsc::Receiver<ContainerStateCommand>,
}
impl ContainerStateActor {
/// 创建新的 Actor 和 Handle
pub fn new() -> (Self, ContainerStateHandle) {
let (sender, receiver) = mpsc::channel(CHANNEL_BUFFER_SIZE);
let actor = Self {
containers: HashMap::new(),
receiver,
};
let handle = ContainerStateHandle { sender };
(actor, handle)
}
/// 运行 Actor 事件循环
///
/// 这个方法应该在 `tokio::spawn` 中调用
pub async fn run(mut self) {
debug!("🚀 [ACTOR] ContainerStateActor started");
while let Some(cmd) = self.receiver.recv().await {
self.handle_command(cmd);
}
debug!("🛑 [ACTOR] ContainerStateActor stopped (all senders dropped)");
}
/// 处理单个命令
fn handle_command(&mut self, cmd: ContainerStateCommand) {
match cmd {
ContainerStateCommand::Get { key, reply } => {
let result = self.containers.get(&key).cloned();
if reply.send(result).is_err() {
warn!("[ACTOR] Get reply channel closed");
}
}
ContainerStateCommand::Insert { key, info } => {
self.containers.insert(key, info);
}
ContainerStateCommand::Remove { key, reply } => {
let result = self.containers.remove(&key);
if reply.send(result).is_err() {
warn!("[ACTOR] Remove reply channel closed");
}
}
ContainerStateCommand::List { reply } => {
let result: Vec<_> = self.containers.values().cloned().collect();
if reply.send(result).is_err() {
warn!("[ACTOR] List reply channel closed");
}
}
ContainerStateCommand::Keys { reply } => {
let result: Vec<_> = self.containers.keys().cloned().collect();
if reply.send(result).is_err() {
warn!("[ACTOR] Keys reply channel closed");
}
}
ContainerStateCommand::Len { reply } => {
if reply.send(self.containers.len()).is_err() {
warn!("[ACTOR] Len reply channel closed");
}
}
ContainerStateCommand::Contains { key, reply } => {
if reply.send(self.containers.contains_key(&key)).is_err() {
warn!("[ACTOR] Contains reply channel closed");
}
}
ContainerStateCommand::UpdateWith {
key,
updated_info,
reply,
} => {
let existed = if let std::collections::hash_map::Entry::Occupied(mut e) =
self.containers.entry(key)
{
e.insert(updated_info);
true
} else {
false
};
if reply.send(existed).is_err() {
warn!("[ACTOR] UpdateWith reply channel closed");
}
}
ContainerStateCommand::RemoveIfContainerId {
key,
container_id,
reply,
} => {
let should_remove = if let Some(info) = self.containers.get(&key) {
info.container_id == container_id
} else {
false
};
let result = if should_remove {
self.containers.remove(&key)
} else {
None
};
if reply.send(result).is_err() {
warn!("[ACTOR] RemoveIfContainerId reply channel closed");
}
}
}
}
}
/// 容器状态句柄
///
/// 可克隆,用于与 Actor 通信
#[derive(Clone)]
pub struct ContainerStateHandle {
sender: mpsc::Sender<ContainerStateCommand>,
}
impl ContainerStateHandle {
/// 获取容器信息
pub async fn get(&self, key: &str) -> Option<DockerContainerInfo> {
let (reply, rx) = oneshot::channel();
if self
.sender
.send(ContainerStateCommand::Get {
key: key.to_string(),
reply,
})
.await
.is_err()
{
error!("[HANDLE] Failed to send Get command - actor stopped");
return None;
}
rx.await.unwrap_or(None)
}
/// 插入/更新容器信息
pub async fn insert(&self, key: String, info: DockerContainerInfo) {
if self
.sender
.send(ContainerStateCommand::Insert { key, info })
.await
.is_err()
{
error!("[HANDLE] Failed to send Insert command - actor stopped");
}
}
/// 移除容器信息
pub async fn remove(&self, key: &str) -> Option<DockerContainerInfo> {
let (reply, rx) = oneshot::channel();
if self
.sender
.send(ContainerStateCommand::Remove {
key: key.to_string(),
reply,
})
.await
.is_err()
{
error!("[HANDLE] Failed to send Remove command - actor stopped");
return None;
}
rx.await.unwrap_or(None)
}
/// 获取所有容器列表
pub async fn list(&self) -> Vec<DockerContainerInfo> {
let (reply, rx) = oneshot::channel();
if self
.sender
.send(ContainerStateCommand::List { reply })
.await
.is_err()
{
error!("[HANDLE] Failed to send List command - actor stopped");
return Vec::new();
}
rx.await.unwrap_or_default()
}
/// 获取所有 key 列表
pub async fn keys(&self) -> Vec<String> {
let (reply, rx) = oneshot::channel();
if self
.sender
.send(ContainerStateCommand::Keys { reply })
.await
.is_err()
{
error!("[HANDLE] Failed to send Keys command - actor stopped");
return Vec::new();
}
rx.await.unwrap_or_default()
}
/// 获取容器数量
pub async fn len(&self) -> usize {
let (reply, rx) = oneshot::channel();
if self
.sender
.send(ContainerStateCommand::Len { reply })
.await
.is_err()
{
error!("[HANDLE] Failed to send Len command - actor stopped");
return 0;
}
rx.await.unwrap_or(0)
}
/// 检查是否为空
pub async fn is_empty(&self) -> bool {
self.len().await == 0
}
/// 检查 key 是否存在
pub async fn contains_key(&self, key: &str) -> bool {
let (reply, rx) = oneshot::channel();
if self
.sender
.send(ContainerStateCommand::Contains {
key: key.to_string(),
reply,
})
.await
.is_err()
{
error!("[HANDLE] Failed to send Contains command - actor stopped");
return false;
}
rx.await.unwrap_or(false)
}
/// 条件更新:如果 key 存在则更新
///
/// 返回 true 表示Update succeededfalse 表示 key 不存在
pub async fn update_if_exists(&self, key: &str, info: DockerContainerInfo) -> bool {
let (reply, rx) = oneshot::channel();
if self
.sender
.send(ContainerStateCommand::UpdateWith {
key: key.to_string(),
updated_info: info,
reply,
})
.await
.is_err()
{
error!("[HANDLE] Failed to send UpdateWith command - actor stopped");
return false;
}
rx.await.unwrap_or(false)
}
/// 条件移除:只有当 container_id 匹配时才移除
///
/// 防止在清理时误删刚重启的容器CAS 操作)
pub async fn remove_if_container_id(
&self,
key: &str,
container_id: &str,
) -> Option<DockerContainerInfo> {
let (reply, rx) = oneshot::channel();
if self
.sender
.send(ContainerStateCommand::RemoveIfContainerId {
key: key.to_string(),
container_id: container_id.to_string(),
reply,
})
.await
.is_err()
{
error!("[HANDLE] Failed to send RemoveIfContainerId command - actor stopped");
return None;
}
rx.await.unwrap_or(None)
}
}
impl std::fmt::Debug for ContainerStateHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ContainerStateHandle")
.field("sender_closed", &self.sender.is_closed())
.finish()
}
}
// Tests removed intentionally.
// Integration tests should be implemented in `tests/` directory if needed.

View File

@@ -0,0 +1,600 @@
//! 统一的容器停止模块
//!
//! 提供两种容器停止策略:
//! 1. 启动时清理startup_cleanup用于服务启动时清理遗留容器
//! - 使用5秒超时
//! - 过滤409冲突错误容器已在删除中
//! - 不阻塞服务启动
//!
//! 2. 运行时清理runtime_cleanup用于运行时快速清理容器
//! - 使用3秒优雅停止超时
//! - 超时后立即强制停止
//! - 快速释放资源
//!
//! # 使用示例
//!
//! ```rust,no_run
//! use docker_manager::container_stop;
//! use docker_manager::DockerManager;
//! use std::sync::Arc;
//!
//! # async fn example() -> anyhow::Result<()> {
//! # let docker_manager = Arc::new(DockerManager::new(Default::default()).await?);
//!
//! // 启动时清理
//! let result = container_stop::startup_cleanup_containers(
//! &docker_manager,
//! "rcoder-agent-*"
//! ).await?;
//!
//! // 运行时清理单个容器
//! container_stop::runtime_cleanup_container(
//! &docker_manager,
//! "container_id_123"
//! ).await?;
//! # Ok(())
//! # }
//! ```
use crate::{CleanupResult, ContainerRemovalFailure, DockerError, DockerManager, DockerResult};
use std::sync::Arc;
use std::time::Instant;
use tracing::{info, warn};
/// 启动清理超时时间(秒)
///
/// 启动时使用较短的超时时间,快速清理遗留容器
const STARTUP_CLEANUP_TIMEOUT_SECONDS: u64 = 5;
/// 运行时清理超时时间(秒)
///
/// 运行时给容器3秒优雅退出时间然后强制停止
const RUNTIME_CLEANUP_TIMEOUT_SECONDS: u64 = 3;
/// 容器停止后的等待时间(毫秒)
///
/// 给Docker一些时间完成清理操作
const POST_STOP_WAIT_MS: u64 = 100;
/// 启动时容器清理策略
///
/// 用于服务启动时清理遗留的容器。此函数会:
/// - 查找匹配指定模式的所有容器
/// - 🚀 并发停止所有容器(提高清理速度)
/// - 使用5秒超时停止每个容器
/// - 过滤409冲突错误容器已在删除中
/// - 返回详细的清理统计信息
///
/// # Arguments
///
/// * `docker_manager` - Docker管理器实例
/// * `pattern` - 容器名称匹配模式(如 "rcoder-agent-*"
///
/// # Returns
///
/// 返回 `CleanupResult` 包含清理统计信息
///
/// # Examples
///
/// ```rust,no_run
/// use docker_manager::container_stop;
/// use docker_manager::DockerManager;
/// use std::sync::Arc;
///
/// # async fn example() -> anyhow::Result<()> {
/// # let docker_manager = Arc::new(DockerManager::new(Default::default()).await?);
///
/// let result = container_stop::startup_cleanup_containers(
/// &docker_manager,
/// "rcoder-agent-*"
/// ).await?;
///
/// println!("cleanup message {} message container", result.successfully_removed);
/// # Ok(())
/// # }
/// ```
pub async fn startup_cleanup_containers(
docker_manager: &Arc<DockerManager>,
pattern: &str,
) -> DockerResult<CleanupResult> {
info!(
"[STARTUP_CLEANUP] Starting cleanup container: pattern={}",
pattern
);
let start_time = Instant::now();
// 查找匹配模式的容器
let matched_containers = docker_manager.list_containers_with_pattern(pattern).await?;
let total_found = matched_containers.len();
info!(
"[STARTUP_CLEANUP] Found {} containers",
total_found
);
if total_found == 0 {
return Ok(CleanupResult {
total_found: 0,
successfully_removed: 0,
failed_removals: 0,
skipped_running: 0,
removed_container_ids: Vec::new(),
failed_removals_details: Vec::new(),
duration_ms: start_time.elapsed().as_millis() as u64,
});
}
let mut successfully_removed = 0;
let mut failed_removals = 0;
let mut removed_container_ids = Vec::new();
let mut failed_removals_details = Vec::new();
// 🚀 并发停止所有容器
let mut tasks = Vec::new();
for container in &matched_containers {
if let Some(container_id) = &container.id {
let container_name = container
.names
.as_ref()
.and_then(|names| names.first())
.map(|n| n.trim_start_matches('/'))
.unwrap_or("unknown")
.to_string();
let docker_manager_clone = Arc::clone(docker_manager);
let container_id_clone = container_id.clone();
let task = tokio::spawn(async move {
let result =
stop_container_startup_mode(&docker_manager_clone, &container_id_clone).await;
(container_id_clone, container_name, result)
});
tasks.push(task);
}
}
// 等待所有任务完成
for task in tasks {
if let Ok((container_id, container_name, result)) = task.await {
match result {
Ok(_) => {
successfully_removed += 1;
removed_container_ids.push(container_id.clone());
info!(
"[STARTUP_CLEANUP] Container cleanup succeeded: container_id={}, name={}",
container_id, container_name
);
}
Err(e) => {
// 检查是否为409冲突错误
if is_409_conflict_error(&e) {
info!(
"[STARTUP_CLEANUP] Container already being removed, skipping: container_id={}, name={}",
container_id, container_name
);
// 409错误不计入失败统计
successfully_removed += 1;
removed_container_ids.push(container_id);
} else {
failed_removals += 1;
warn!(
"[STARTUP_CLEANUP] Container cleanup failed: container_id={}, name={}, error={}",
container_id, container_name, e
);
failed_removals_details.push(ContainerRemovalFailure {
container_id,
container_name,
error_message: e.to_string(),
});
}
}
}
}
}
let duration_ms = start_time.elapsed().as_millis() as u64;
info!(
"[STARTUP_CLEANUP] Cleanup completed: total={}, success={}, failed={}, duration={}ms",
total_found, successfully_removed, failed_removals, duration_ms
);
Ok(CleanupResult {
total_found,
successfully_removed,
failed_removals,
skipped_running: 0, // 启动清理不跳过运行中的容器
removed_container_ids,
failed_removals_details,
duration_ms,
})
}
/// 停止单个容器(启动模式)
///
/// 使用启动清理的超时设置停止容器
///
/// # Arguments
///
/// * `docker_manager` - Docker管理器实例
/// * `container_id` - 容器ID
///
/// # Returns
///
/// 成功返回 `Ok(())`,失败返回 `DockerError`
async fn stop_container_startup_mode(
docker_manager: &Arc<DockerManager>,
container_id: &str,
) -> DockerResult<()> {
docker_manager
.stop_container_by_id_with_timeout(container_id, STARTUP_CLEANUP_TIMEOUT_SECONDS)
.await?;
// 给Docker一些时间完成清理
tokio::time::sleep(tokio::time::Duration::from_millis(POST_STOP_WAIT_MS)).await;
Ok(())
}
/// 检查是否为409冲突错误
///
/// 409错误表示容器已经在删除过程中这在启动清理时是正常情况
///
/// # Arguments
///
/// * `error` - Docker错误
///
/// # Returns
///
/// 如果是409冲突错误返回 `true`,否则返回 `false`
fn is_409_conflict_error(error: &DockerError) -> bool {
// 检查是否是 Docker 409 冲突错误(容器删除已在进行中)
matches!(
error,
DockerError::BollardError(bollard::errors::Error::DockerResponseServerError {
status_code: 409,
..
})
)
}
/// 运行时容器清理策略(单个容器)
///
/// 用于运行时快速清理单个容器。此函数会:
/// - 使用3秒优雅停止超时
/// - 超时后立即强制停止
/// - 快速释放资源
///
/// # Arguments
///
/// * `docker_manager` - Docker管理器实例
/// * `container_id` - 容器ID
///
/// # Returns
///
/// 成功返回 `Ok(())`,失败返回 `DockerError`
///
/// # Examples
///
/// ```rust,no_run
/// use docker_manager::container_stop;
/// use docker_manager::DockerManager;
/// use std::sync::Arc;
///
/// # async fn example() -> anyhow::Result<()> {
/// # let docker_manager = Arc::new(DockerManager::new(Default::default()).await?);
///
/// container_stop::runtime_cleanup_container(
/// &docker_manager,
/// "container_id_123"
/// ).await?;
/// # Ok(())
/// # }
/// ```
pub async fn runtime_cleanup_container(
docker_manager: &Arc<DockerManager>,
container_id: &str,
) -> DockerResult<()> {
info!(
"[RUNTIME_CLEANUP] Starting to stop container: container_id={}",
container_id
);
match stop_container_runtime_mode(docker_manager, container_id).await {
Ok(_) => {
info!(
"[RUNTIME_CLEANUP] Container stop succeeded: container_id={}",
container_id
);
Ok(())
}
Err(e) => {
warn!(
"[RUNTIME_CLEANUP] Container stop failed: container_id={}, error={}",
container_id, e
);
Err(e)
}
}
}
/// 运行时容器清理策略(批量)
///
/// 用于运行时批量清理多个容器。此函数会:
/// - 🚀 并发停止所有容器(提高清理速度)
/// - 使用3秒优雅停止超时
/// - 超时后立即强制停止
/// - 返回详细的清理统计信息
///
/// # Arguments
///
/// * `docker_manager` - Docker管理器实例
/// * `container_ids` - 容器ID列表
///
/// # Returns
///
/// 返回 `CleanupResult` 包含清理统计信息
///
/// # Examples
///
/// ```rust,no_run
/// use docker_manager::container_stop;
/// use docker_manager::DockerManager;
/// use std::sync::Arc;
///
/// # async fn example() -> anyhow::Result<()> {
/// # let docker_manager = Arc::new(DockerManager::new(Default::default()).await?);
///
/// let container_ids = vec!["id1".to_string(), "id2".to_string()];
/// let result = container_stop::runtime_cleanup_containers(
/// &docker_manager,
/// container_ids
/// ).await?;
/// # Ok(())
/// # }
/// ```
pub async fn runtime_cleanup_containers(
docker_manager: &Arc<DockerManager>,
container_ids: Vec<String>,
) -> DockerResult<CleanupResult> {
info!(
"[RUNTIME_CLEANUP] Starting batch container cleanup: count={}",
container_ids.len()
);
let start_time = Instant::now();
let total_found = container_ids.len();
let mut successfully_removed = 0;
let mut failed_removals = 0;
let mut removed_container_ids = Vec::new();
let mut failed_removals_details = Vec::new();
// 🚀 并发停止所有容器
let mut tasks = Vec::new();
for container_id in &container_ids {
let docker_manager_clone = Arc::clone(docker_manager);
let container_id_clone = container_id.clone();
let task = tokio::spawn(async move {
let result =
stop_container_runtime_mode(&docker_manager_clone, &container_id_clone).await;
(container_id_clone, result)
});
tasks.push(task);
}
// 等待所有任务完成
for task in tasks {
if let Ok((container_id, result)) = task.await {
match result {
Ok(_) => {
successfully_removed += 1;
removed_container_ids.push(container_id.clone());
info!(
"[RUNTIME_CLEANUP] Container cleanup succeeded: container_id={}",
container_id
);
}
Err(e) => {
failed_removals += 1;
warn!(
"[RUNTIME_CLEANUP] Container cleanup failed: container_id={}, error={}",
container_id, e
);
failed_removals_details.push(ContainerRemovalFailure {
container_id: container_id.clone(),
container_name: container_id.clone(), // 批量清理时可能不知道名称
error_message: e.to_string(),
});
}
}
}
}
let duration_ms = start_time.elapsed().as_millis() as u64;
info!(
"[RUNTIME_CLEANUP] Batch cleanup completed: total={}, success={}, failed={}, duration={}ms",
total_found, successfully_removed, failed_removals, duration_ms
);
Ok(CleanupResult {
total_found,
successfully_removed,
failed_removals,
skipped_running: 0, // 运行时清理不跳过运行中的容器
removed_container_ids,
failed_removals_details,
duration_ms,
})
}
/// 停止单个容器(运行时模式)
///
/// 使用运行时清理的超时设置停止容器
///
/// # Arguments
///
/// * `docker_manager` - Docker管理器实例
/// * `container_id` - 容器ID
///
/// # Returns
///
/// 成功返回 `Ok(())`,失败返回 `DockerError`
async fn stop_container_runtime_mode(
docker_manager: &Arc<DockerManager>,
container_id: &str,
) -> DockerResult<()> {
docker_manager
.stop_container_by_id_with_timeout(container_id, RUNTIME_CLEANUP_TIMEOUT_SECONDS)
.await?;
// 给Docker一些时间完成清理
tokio::time::sleep(tokio::time::Duration::from_millis(POST_STOP_WAIT_MS)).await;
Ok(())
}
/// 为所有启用的服务构建容器清理模式列表
///
/// 从多镜像配置中获取所有启用的服务类型,并生成对应的容器名称模式。
/// 使用 ServiceImageConfig.container_prefix() 获取配置的前缀,确保与容器创建时使用的前缀一致。
///
/// # Arguments
///
/// * `multi_image_config` - 多镜像配置
///
/// # Returns
///
/// 返回容器名称模式列表,如 `["rcoder-agent-*", "rcoder-computer-agent-runner-*"]`
///
/// # Examples
///
/// ```rust,no_run
/// use docker_manager::container_stop;
/// use shared_types;
///
/// let config = shared_types::create_default_multi_image_config();
/// let patterns = container_stop::get_container_patterns_for_enabled_services(&config);
/// println!("container message : {:?}", patterns);
/// ```
pub fn get_container_patterns_for_enabled_services(
multi_image_config: &shared_types::MultiImageConfig,
) -> Vec<String> {
// 直接遍历 services获取启用的服务配置并使用其 container_prefix()
multi_image_config
.services
.values()
.filter(|config| config.enabled)
.map(|config| {
let prefix = config.container_prefix();
let pattern = format!("{}-*", prefix);
tracing::debug!(
"🔍 [CLEANUP_PATTERN] Service type: {:?}, using prefix: {}, pattern: {}",
config.service_type,
prefix,
pattern
);
pattern
})
.collect()
}
/// 清理所有启用服务的容器(启动时清理)
///
/// 自动从配置中获取所有启用的服务,并清理对应的容器。此函数会:
/// - 从配置中读取启用的服务类型
/// - 为每个服务类型生成容器模式
/// - 并行清理多个服务类型的容器
/// - 聚合所有清理结果
///
/// # Arguments
///
/// * `docker_manager` - Docker管理器实例
/// * `multi_image_config` - 多镜像配置
///
/// # Returns
///
/// 返回聚合的 `CleanupResult` 包含所有服务的清理统计信息
///
/// # Examples
///
/// ```rust,no_run
/// use docker_manager::container_stop;
/// use docker_manager::DockerManager;
/// use shared_types;
/// use std::sync::Arc;
///
/// # async fn example() -> anyhow::Result<()> {
/// # let docker_manager = Arc::new(DockerManager::new(Default::default()).await?);
/// let config = shared_types::create_default_multi_image_config();
///
/// let result = container_stop::startup_cleanup_all_enabled_services(
/// &docker_manager,
/// &config
/// ).await?;
///
/// println!("cleanup message {} message container", result.successfully_removed);
/// # Ok(())
/// # }
/// ```
pub async fn startup_cleanup_all_enabled_services(
docker_manager: &Arc<DockerManager>,
multi_image_config: &shared_types::MultiImageConfig,
) -> DockerResult<CleanupResult> {
let patterns = get_container_patterns_for_enabled_services(multi_image_config);
if patterns.is_empty() {
warn!("No patterns, skip container cleanup");
return Ok(CleanupResult::default());
}
info!("🧹 Starting cleanup container: {:?}", patterns);
let start_time = Instant::now();
// 并行清理多个服务类型的容器
let cleanup_tasks: Vec<_> = patterns
.into_iter()
.map(|pattern| {
let docker_manager = docker_manager.clone();
let pattern_clone = pattern.clone();
tokio::spawn(async move {
startup_cleanup_containers(&docker_manager, &pattern_clone).await
})
})
.collect();
// 聚合所有清理结果
let mut aggregated_result = CleanupResult::default();
for task in cleanup_tasks {
match task.await {
Ok(Ok(result)) => {
aggregated_result.total_found += result.total_found;
aggregated_result.successfully_removed += result.successfully_removed;
aggregated_result.failed_removals += result.failed_removals;
aggregated_result.skipped_running += result.skipped_running;
aggregated_result
.removed_container_ids
.extend(result.removed_container_ids);
aggregated_result
.failed_removals_details
.extend(result.failed_removals_details);
}
Ok(Err(e)) => {
warn!("cleanup container failed: {}", e);
}
Err(e) => {
warn!("cleanup failed: {}", e);
}
}
}
aggregated_result.duration_ms = start_time.elapsed().as_millis() as u64;
info!(
"[MULTI_SERVICE_CLEANUP] Multi-service cleanup completed: total={}, success={}, failed={}, duration={}ms",
aggregated_result.total_found,
aggregated_result.successfully_removed,
aggregated_result.failed_removals,
aggregated_result.duration_ms
);
Ok(aggregated_result)
}

View File

@@ -0,0 +1,125 @@
//! HTTP 健康检查
//!
//! 从 docker_container_agent.rs 迁移
use crate::{DockerError, DockerResult};
use reqwest::Client;
use std::time::Duration;
use tokio::time::timeout;
use tracing::{debug, info};
/// HTTP 健康检查器
pub struct HttpHealthChecker {
client: Client,
max_attempts: u32,
timeout_seconds: u64,
}
impl HttpHealthChecker {
/// 创建新的健康检查器
///
/// # Arguments
/// * `max_attempts` - 最大尝试次数
/// * `timeout_seconds` - 每次尝试的超时时间(秒)
pub fn new(max_attempts: u32, timeout_seconds: u64) -> Self {
Self {
client: Client::new(),
max_attempts,
timeout_seconds,
}
}
/// 默认配置的健康检查器(60次每次2秒总计约180秒)
/// 容器启动包含 MCP Proxy 等服务,可能需要 60-90 秒
pub fn default_checker() -> Self {
Self::new(60, 2)
}
/// 等待服务就绪
///
/// # Arguments
/// * `base_url` - 服务基础URL (如: "http://172.17.0.2:8086")
/// * `health_path` - 健康检查路径 (默认: "/health")
///
/// # Returns
/// * `DockerResult<()>` - 成功或超时错误
pub async fn wait_for_ready(
&self,
base_url: &str,
health_path: Option<&str>,
) -> DockerResult<()> {
let health_url = format!(
"{}/{}",
base_url.trim_end_matches('/'),
health_path.unwrap_or("health").trim_start_matches('/')
);
info!("Health check started: {}", health_url);
for attempt in 0..self.max_attempts {
match timeout(
Duration::from_secs(self.timeout_seconds),
self.client.get(&health_url).send(),
)
.await
{
Ok(Ok(response)) if response.status().is_success() => {
info!("health check passed");
return Ok(());
}
Ok(Ok(response)) => {
debug!(
"Service returned non-success status: {}, waiting... ({}/{})",
response.status(),
attempt + 1,
self.max_attempts
);
}
Ok(Err(e)) => {
debug!(
"Connection failed: {}, continuing to wait... ({}/{})",
e,
attempt + 1,
self.max_attempts
);
}
Err(_) => {
debug!(
"Connection timeout, continuing to wait... ({}/{})",
attempt + 1,
self.max_attempts
);
}
}
// 每 10 次尝试输出一次 info 日志
if (attempt + 1) % 10 == 0 {
info!(
"Still waiting for service to start... ({}/{})",
attempt + 1,
self.max_attempts
);
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
Err(DockerError::ContainerStartError(format!(
"Wait for service startup timeout: {} (attempted {} times)",
health_url, self.max_attempts
)))
}
}
/// 便捷函数: 等待服务就绪(使用默认配置)
///
/// # Arguments
/// * `base_url` - 服务基础URL
///
/// # Returns
/// * `DockerResult<()>` - 成功或超时错误
pub async fn wait_for_service_ready(base_url: &str) -> DockerResult<()> {
HttpHealthChecker::default_checker()
.wait_for_ready(base_url, None)
.await
}

View File

@@ -0,0 +1,7 @@
//! 容器健康检查模块
pub mod http_health;
pub mod service_health;
pub use http_health::*;
pub use service_health::*;

View File

@@ -0,0 +1,264 @@
//! 服务健康检查模块
//!
//! 提供服务层面的健康检查功能,包括 HTTP 健康端点和 gRPC 连接检查。
//! 这是对 Docker 容器状态检查的补充,用于确认容器内服务是否真正可用。
use chrono::{DateTime, Utc};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::time::timeout;
use tracing::{debug, warn};
/// 默认 HTTP 健康检查端口
pub const DEFAULT_HTTP_HEALTH_PORT: u16 = 8086;
/// 默认 gRPC 服务端口
pub const DEFAULT_GRPC_PORT: u16 = 50051;
/// 服务健康检查超时时间(秒)
const HEALTH_CHECK_TIMEOUT_SECS: u64 = 3;
/// 服务健康状态
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceHealthStatus {
/// HTTP 健康端点是否可达
pub http_healthy: bool,
/// gRPC 端口是否可连接
pub grpc_healthy: bool,
/// 上次检查时间
pub last_check_time: DateTime<Utc>,
/// 连续失败次数HTTP 和 gRPC 都失败算一次)
/// TODO: 用于后续自动重启功能
pub consecutive_failures: u32,
}
impl ServiceHealthStatus {
/// 创建新的健康状态(初始化为未知状态)
pub fn new() -> Self {
Self {
http_healthy: false,
grpc_healthy: false,
last_check_time: Utc::now(),
consecutive_failures: 0,
}
}
/// 检查服务是否完全健康HTTP 和 gRPC 都正常)
pub fn is_fully_healthy(&self) -> bool {
self.http_healthy && self.grpc_healthy
}
/// 检查服务是否部分健康(至少有一个正常)
pub fn is_partially_healthy(&self) -> bool {
self.http_healthy || self.grpc_healthy
}
}
impl Default for ServiceHealthStatus {
fn default() -> Self {
Self::new()
}
}
/// 服务健康检查器
pub struct ServiceHealthChecker {
client: Client,
http_port: u16,
grpc_port: u16,
timeout_secs: u64,
}
impl ServiceHealthChecker {
/// 创建新的健康检查器
pub fn new() -> Self {
Self {
client: Client::builder()
.timeout(Duration::from_secs(HEALTH_CHECK_TIMEOUT_SECS))
.build()
.unwrap_or_else(|_| Client::new()),
http_port: DEFAULT_HTTP_HEALTH_PORT,
grpc_port: DEFAULT_GRPC_PORT,
timeout_secs: HEALTH_CHECK_TIMEOUT_SECS,
}
}
/// 使用自定义端口创建检查器
pub fn with_ports(http_port: u16, grpc_port: u16) -> Self {
Self {
http_port,
grpc_port,
..Self::new()
}
}
/// 检查 HTTP 健康端点
///
/// # Arguments
/// * `ip` - 容器 IP 地址
///
/// # Returns
/// * `true` - 健康端点返回成功状态码
/// * `false` - 连接失败或返回错误状态码
pub async fn check_http_health(&self, ip: &str) -> bool {
let url = format!("http://{}:{}/health", ip, self.http_port);
match timeout(
Duration::from_secs(self.timeout_secs),
self.client.get(&url).send(),
)
.await
{
Ok(Ok(response)) => {
let is_healthy = response.status().is_success();
debug!(
"HTTP health check {}: status={}, healthy={}",
url,
response.status(),
is_healthy
);
is_healthy
}
Ok(Err(e)) => {
debug!("HTTP health check failed {}: {}", url, e);
false
}
Err(_) => {
debug!("HTTP health check timeout {}", url);
false
}
}
}
/// 检查 gRPC 端口连通性
///
/// 通过 TCP 连接测试 gRPC 端口是否可达。
/// 注意:这只是连接测试,不执行实际的 gRPC 健康检查协议。
///
/// # Arguments
/// * `ip` - 容器 IP 地址
///
/// # Returns
/// * `true` - 能够建立 TCP 连接
/// * `false` - 连接失败或超时
pub async fn check_grpc_connectivity(&self, ip: &str) -> bool {
let addr = format!("{}:{}", ip, self.grpc_port);
match timeout(
Duration::from_secs(self.timeout_secs),
TcpStream::connect(&addr),
)
.await
{
Ok(Ok(_stream)) => {
debug!("gRPC port connection: {}", addr);
true
}
Ok(Err(e)) => {
debug!("gRPC portconnectionfailed {}: {}", addr, e);
false
}
Err(_) => {
debug!("gRPC portconnectiontimeout {}", addr);
false
}
}
}
/// 执行完整的服务健康检查
///
/// 同时检查 HTTP 健康端点和 gRPC 端口连通性。
///
/// # Arguments
/// * `container_ip` - 容器 IP 地址
/// * `previous_failures` - 之前的连续失败次数(用于累加)
///
/// # Returns
/// * `ServiceHealthStatus` - 包含所有检查结果的健康状态
pub async fn check_service(
&self,
container_ip: &str,
previous_failures: u32,
) -> ServiceHealthStatus {
// 并行执行两个检查
let (http_healthy, grpc_healthy) = tokio::join!(
self.check_http_health(container_ip),
self.check_grpc_connectivity(container_ip)
);
let is_fully_healthy = http_healthy && grpc_healthy;
// 更新连续失败次数
let consecutive_failures = if is_fully_healthy {
0 // 完全健康,重置计数
} else {
previous_failures + 1
};
if !is_fully_healthy {
warn!(
"Service health check: IP={}, HTTP={}, gRPC={}, consecutive_failures={}",
container_ip, http_healthy, grpc_healthy, consecutive_failures
);
}
ServiceHealthStatus {
http_healthy,
grpc_healthy,
last_check_time: Utc::now(),
consecutive_failures,
}
}
}
impl Default for ServiceHealthChecker {
fn default() -> Self {
Self::new()
}
}
/// 便捷函数:使用默认配置执行服务健康检查
pub async fn check_service_health(container_ip: &str) -> ServiceHealthStatus {
ServiceHealthChecker::new()
.check_service(container_ip, 0)
.await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_health_status_new() {
let status = ServiceHealthStatus::new();
assert!(!status.http_healthy);
assert!(!status.grpc_healthy);
assert_eq!(status.consecutive_failures, 0);
}
#[test]
fn test_is_fully_healthy() {
let mut status = ServiceHealthStatus::new();
assert!(!status.is_fully_healthy());
status.http_healthy = true;
assert!(!status.is_fully_healthy());
status.grpc_healthy = true;
assert!(status.is_fully_healthy());
}
#[test]
fn test_is_partially_healthy() {
let mut status = ServiceHealthStatus::new();
assert!(!status.is_partially_healthy());
status.http_healthy = true;
assert!(status.is_partially_healthy());
status.http_healthy = false;
status.grpc_healthy = true;
assert!(status.is_partially_healthy());
}
}

View File

@@ -0,0 +1,155 @@
//! 镜像选择器
//!
//! 根据服务类型选择合适的 Docker 镜像。
//! 简化版本针对只有2种镜像的静态映射场景进行了优化。
use crate::utils::DockerUtils;
use crate::{DockerError, DockerResult};
use shared_types::{MultiImageConfig, ProjectImageOverrides, ServiceType};
use tracing::{debug, info, warn};
/// 简化的镜像选择器
///
/// 根据服务类型选择合适的 Docker 镜像。
/// 针对2种镜像的静态映射场景进行了优化移除了不必要的缓存。
/// 强制要求明确指定服务类型,不支持默认值。
pub struct ImageSelector {
/// 多镜像配置
config: MultiImageConfig,
/// 当前平台
platform: String,
}
impl ImageSelector {
/// 创建新的镜像选择器
pub fn new(config: MultiImageConfig) -> Self {
let platform = DockerUtils::get_optimal_platform();
debug!("created selector for: {}", platform);
Self { config, platform }
}
/// 根据服务类型和项目配置选择镜像
///
/// 注意service_type 不能为空,必须明确指定。
/// 会自动验证服务是否已启用。
/// 简化版本:直接计算镜像名称,无缓存
pub async fn select_image(
&self,
service_type: &ServiceType,
project_overrides: Option<&ProjectImageOverrides>,
) -> DockerResult<String> {
// 强制验证service_type 必须明确指定并启用
if !self.is_service_enabled(service_type) {
return Err(DockerError::ConfigurationError(format!(
"service type '{}' is not enabled or configuration does not exist",
service_type
)));
}
// 直接计算镜像名称,无需缓存
let image_name = self
.select_service_image(service_type, project_overrides)
.await?;
info!(
"Selected image: {} (service: {}, platform: {})",
image_name, service_type, self.platform
);
Ok(image_name)
}
/// 获取服务配置
pub async fn get_service_config(
&self,
service_type: &ServiceType,
) -> DockerResult<shared_types::ServiceImageConfig> {
// 强制验证service_type 必须明确指定并启用
if !self.is_service_enabled(service_type) {
return Err(DockerError::ConfigurationError(format!(
"service type '{}' is not enabled or configuration does not exist",
service_type
)));
}
// 从配置中获取服务配置
let service_key = service_type.to_string();
match self.config.services.get(&service_key) {
Some(service_config) => {
info!("Get config succeeded: {}", service_key);
Ok(service_config.clone())
}
None => Err(DockerError::ConfigurationError(format!(
"configuration for service type '{}' does not exist",
service_type
))),
}
}
/// 检查服务是否已启用和配置
pub fn is_service_enabled(&self, service_type: &ServiceType) -> bool {
let service_key = service_type.to_string();
info!(
"[IMAGE_SELECTOR] Checking if service is enabled: service_type={:?}, service_key={}",
service_type, service_key
);
if let Some(service_config) = self.config.services.get(&service_key) {
info!(
"[IMAGE_SELECTOR] Service found: enabled={}, arm64_image={:?}",
service_config.enabled, service_config.arm64_image
);
service_config.enabled
} else {
warn!(
"[IMAGE_SELECTOR] Service type '{}' not found in config, available services: {:?}",
service_key,
self.config.services.keys().collect::<Vec<_>>()
);
false
}
}
/// 从服务特定配置选择镜像
/// 简化版本针对2种镜像的静态映射
async fn select_service_image(
&self,
service_type: &ServiceType,
_project_overrides: Option<&ProjectImageOverrides>,
) -> DockerResult<String> {
let service_key = service_type.to_string();
// 1. 优先使用服务特定配置
if let Some(service_config) = self.config.services.get(&service_key) {
// 服务级通用镜像(最高优先级)
if let Some(image) = &service_config.image {
debug!(" using image: {}", image);
return Ok(image.clone());
}
// 平台特定镜像
if self.platform == "linux/arm64" {
if let Some(arm64_image) = &service_config.arm64_image {
debug!(" using ARM64 image: {}", arm64_image);
return Ok(arm64_image.clone());
}
} else if let Some(amd64_image) = &service_config.amd64_image {
debug!(" using AMD64 image: {}", amd64_image);
return Ok(amd64_image.clone());
}
}
// 2. 使用全局默认配置
if let Some(default_image) = &self.config.global_defaults.default_image {
debug!(" using default image: {}", default_image);
return Ok(default_image.clone());
}
// 3. 配置错误:不应该发生,因为默认配置已经设置了镜像
Err(DockerError::ConfigurationError(format!(
"Service type '{}' has no available image config, please check the configuration file",
service_key
)))
}
}

View File

@@ -0,0 +1,303 @@
use thiserror::Error;
pub mod container_self_inspector;
pub mod container_state_actor;
pub mod container_stop;
pub mod image_selector;
pub mod manager;
pub mod path;
pub mod types;
pub mod utils;
// 新增模块
pub mod container_builder;
pub mod health;
pub mod network;
pub mod runtime_selection;
// Runtime abstraction (Docker/K8s selection)
pub mod runtime;
pub use container_self_inspector::*;
pub use container_state_actor::*;
pub use manager::*;
pub use types::*;
pub use utils::*;
// 公共导出新模块
pub use container_builder::{ContainerConfigBuilder, MountProcessor};
pub use health::{
HttpHealthChecker, ServiceHealthChecker, ServiceHealthStatus, wait_for_service_ready,
};
pub use network::{NetworkDetector, build_network_name, parse_project_from_network};
pub use path::{
HostPathResolver, get_host_path_resolver, normalize_path, resolve_container_path_to_host,
};
/// Docker manager error type
#[derive(Error, Debug)]
pub enum DockerError {
#[error("docker connection failed: {0}")]
ConnectionError(String),
#[error("container creation failed: {0}")]
ContainerCreationError(String),
#[error("container start failed: {0}")]
ContainerStartError(String),
#[error("container stop failed: {0}")]
ContainerStopError(String),
#[error("container removal failed: {0}")]
ContainerRemoveError(String),
#[error("image pull failed: {0}")]
ImagePullError(String),
#[error("configuration error: {0}")]
ConfigurationError(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("timestamp parsing failed: {0}")]
InvalidTimestamp(String),
#[error("docker API call timeout: {0}")]
Timeout(String),
#[error("bollard docker error: {0}")]
BollardError(#[from] bollard::errors::Error),
}
/// Docker 管理器结果类型
pub type DockerResult<T> = Result<T, DockerError>;
/// 默认的 Docker 镜像配置常量
pub mod default_images {
/// ARM64 架构的默认镜像
pub const ARM64: &str = "registry.yichamao.com/agent-runner:latest-arm64";
/// AMD64 架构的默认镜像
pub const AMD64: &str = "registry.yichamao.com/agent-runner:latest-amd64";
/// 默认回退镜像(当无法检测架构或架构不匹配时使用)
pub const DEFAULT: &str = "registry.yichamao.com/agent-runner:latest";
}
/// 默认的 Docker 镜像(根据架构自动选择)
///
/// 注意:此函数使用硬编码的默认值,建议使用 `get_docker_image_from_config()`
/// 从配置中读取镜像地址
pub fn default_docker_image() -> String {
let platform = crate::utils::DockerUtils::auto_detect_platform();
match platform.as_str() {
"linux/arm64" => default_images::ARM64.to_string(),
"linux/amd64" => default_images::AMD64.to_string(),
_ => default_images::DEFAULT.to_string(), // 默认回退
}
}
/// 获取默认的 ARM64 镜像
pub fn default_arm64_image() -> String {
default_images::ARM64.to_string()
}
/// 获取默认的 AMD64 镜像
pub fn default_amd64_image() -> String {
default_images::AMD64.to_string()
}
/// 获取默认的回退镜像
pub fn default_fallback_image() -> String {
default_images::DEFAULT.to_string()
}
/// 从 rcoder 配置获取 Docker 镜像
///
/// # 参数
/// * `image` - 通用镜像(优先使用,如果指定则忽略架构特定镜像)
/// * `arm64_image` - ARM64 架构专用镜像
/// * `amd64_image` - AMD64 架构专用镜像
/// * `default_image` - 默认回退镜像(当无法检测架构或架构不匹配时使用)
pub fn get_docker_image_from_config(
image: Option<String>,
arm64_image: Option<String>,
amd64_image: Option<String>,
default_image: Option<String>,
) -> String {
let platform = crate::utils::DockerUtils::auto_detect_platform();
// 优先使用通用镜像
if let Some(img) = image {
return img;
}
// 根据架构使用特定镜像
match platform.as_str() {
"linux/arm64" => arm64_image.unwrap_or_else(|| {
default_image.unwrap_or_else(|| default_images::DEFAULT.to_string())
}),
"linux/amd64" => amd64_image.unwrap_or_else(|| {
default_image.unwrap_or_else(|| default_images::DEFAULT.to_string())
}),
_ => default_image.unwrap_or_else(|| default_images::DEFAULT.to_string()),
}
}
/// 默认的平台(使用自动检测)
pub fn default_platform() -> String {
crate::utils::DockerUtils::auto_detect_platform()
}
/// 默认的工作目录
pub const DEFAULT_WORK_DIR: &str = "/app";
/// 默认的网络模式
pub const DEFAULT_NETWORK_MODE: &str = "bridge";
/// RCoder 专用网络名称(基础名称,不含 project name 前缀)
/// Docker Compose 会自动添加 project name 前缀,实际网络名称为 {project_name}_{network_name}
/// 例如: rcoder_agent-network, myapp_agent-network
///
/// ⚠️ 注意:实际使用时必须动态检测主容器所在的网络,不能硬编码
pub const RCODER_NETWORK_BASE_NAME: &str = "agent-network";
/// 全局 Docker 管理器实例
pub mod global {
use super::*;
#[cfg(feature = "kubernetes")]
use crate::runtime_selection::RuntimeType;
use std::sync::Arc;
use tokio::sync::OnceCell;
use tracing::{debug, info};
/// 全局 DockerManager 单例(用于向后兼容)
static GLOBAL_DOCKER_MANAGER: OnceCell<Arc<DockerManager>> = OnceCell::const_new();
/// 初始化全局 DockerManager
pub async fn init_global_docker_manager() -> DockerResult<()> {
let config = DockerManagerConfig::default();
let manager = Arc::new(DockerManager::new(config).await?);
GLOBAL_DOCKER_MANAGER.set(manager).map_err(|_| {
DockerError::IoError(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"global DockerManager already initialized",
))
})?;
info!("DockerManager initialized");
Ok(())
}
/// 使用自定义配置初始化全局 DockerManager
///
/// 注意:此函数保持向后兼容。对于 K8s 支持,请使用 init_global_runtime()
#[cfg(feature = "kubernetes")]
pub async fn init_global_docker_manager_with_config(
config: DockerManagerConfig,
) -> DockerResult<()> {
let runtime_type = RuntimeType::from_env();
crate::runtime::RuntimeManager::init(config.clone())
.await
.map_err(|e| DockerError::ConfigurationError(e.to_string()))?;
info!("Runtime initialized with config");
if runtime_type == RuntimeType::Docker {
let manager = Arc::new(DockerManager::new(config).await?);
GLOBAL_DOCKER_MANAGER.set(manager).map_err(|_| {
DockerError::IoError(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"global DockerManager already initialized",
))
})?;
info!("DockerManager initialized with config");
}
Ok(())
}
/// 使用自定义配置初始化全局 DockerManager无 K8s 支持)
#[cfg(not(feature = "kubernetes"))]
pub async fn init_global_docker_manager_with_config(
config: DockerManagerConfig,
) -> DockerResult<()> {
// Initialize RuntimeManager so RUNTIME_INSTANCE is set
// This allows RuntimeManager::get() to work in docker compose mode
crate::runtime::RuntimeManager::init(config.clone())
.await
.map_err(|e| DockerError::ConfigurationError(e.to_string()))?;
info!("Runtime initialized with config");
let manager = Arc::new(DockerManager::new(config).await?);
GLOBAL_DOCKER_MANAGER.set(manager).map_err(|_| {
DockerError::IoError(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"global DockerManager already initialized",
))
})?;
info!("DockerManager initialized with config");
Ok(())
}
/// 初始化全局运行时RuntimeManager
///
/// 根据 CONTAINER_RUNTIME 环境变量选择 Docker 或 Kubernetes 运行时
#[cfg(feature = "kubernetes")]
pub async fn init_global_runtime(
config: DockerManagerConfig,
) -> container_runtime_api::ContainerRuntimeResult<()> {
crate::runtime::RuntimeManager::init(config).await
}
/// 获取全局运行时实例Arc<dyn ContainerRuntime>
///
/// 支持 Docker 和 Kubernetes 运行时
#[cfg(feature = "kubernetes")]
pub async fn get_global_runtime(
) -> container_runtime_api::ContainerRuntimeResult<Arc<dyn container_runtime_api::ContainerRuntime>> {
crate::runtime::RuntimeManager::get().await
}
/// 获取全局 DockerManager 实例
///
/// 注意:此函数仅在 Docker 模式下返回有效的 DockerManager。
/// 如果需要同时支持 Docker 和 K8s请使用 get_global_runtime()
///
/// 如果未初始化,会自动初始化
pub async fn get_global_docker_manager() -> DockerResult<Arc<DockerManager>> {
#[cfg(feature = "kubernetes")]
if RuntimeType::from_env() == RuntimeType::Kubernetes {
return Err(DockerError::ConfigurationError(
"DockerManager is unavailable in Kubernetes runtime mode. Use RuntimeManager::get()"
.to_string(),
));
}
if GLOBAL_DOCKER_MANAGER.get().is_none() {
debug!("DockerManager not initialized, starting initialize");
init_global_docker_manager().await?;
}
GLOBAL_DOCKER_MANAGER.get().cloned().ok_or_else(|| {
DockerError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
"unable to get global DockerManager",
))
})
}
/// 使用全局 DockerManager 执行操作(向后兼容)
pub async fn with_global_docker_manager<F, R>(f: F) -> DockerResult<R>
where
F: FnOnce(&Arc<DockerManager>) -> R,
{
let manager = get_global_docker_manager().await?;
Ok(f(&manager))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,185 @@
//! 网络检测功能
//!
//! 从 container_manager.rs 迁移的网络检测逻辑
use crate::{DockerError, DockerResult, RCODER_NETWORK_BASE_NAME};
use bollard::{Docker, query_parameters::InspectContainerOptions};
use tracing::{debug, info, warn};
/// 网络检测器
pub struct NetworkDetector<'a> {
docker: &'a Docker,
}
impl<'a> NetworkDetector<'a> {
/// 创建新的网络检测器
pub fn new(docker: &'a Docker) -> Self {
Self { docker }
}
/// 动态检测当前主容器所在的网络名称
///
/// 通过检查当前容器的网络配置来确定主网络名称
///
/// # Returns
/// * `DockerResult<String>` - 网络名称或错误
pub async fn detect_main_network(&self) -> DockerResult<String> {
// 从 HOSTNAME 环境变量获取容器ID
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 当前容器
let inspect = self
.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
{
// 查找包含 "agent-network" 的网络
for network_name in networks.keys() {
if network_name.contains(RCODER_NETWORK_BASE_NAME) {
info!(" detected network: {}", network_name);
return Ok(network_name.clone());
}
}
// 如果没找到回退到第一个可用网络Docker Compose 默认网络)
if let Some(fallback_name) = networks.keys().next() {
warn!(
"未找到包含 '{}' 的网络,回退使用当前容器的默认网络: {} (可用网络: {:?})",
RCODER_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
)))
}
/// 从容器 labels 获取 Compose 项目名称
///
/// # Arguments
/// * `container_id` - 容器ID或hostname
///
/// # Returns
/// * `DockerResult<Option<String>>` - 项目名称或None
pub async fn get_compose_project_from_labels(
&self,
container_id: &str,
) -> DockerResult<Option<String>> {
let inspect = self
.docker
.inspect_container(container_id, None::<InspectContainerOptions>)
.await
.map_err(|e| DockerError::ConnectionError(format!("failed to get container info: {}", e)))?;
// 从 labels 中获取项目名称
if let Some(labels) = inspect.config.and_then(|c| c.labels) {
// Docker Compose 会添加 com.docker.compose.project 标签
if let Some(project_name) = labels.get("com.docker.compose.project") {
info!(
" container labels get project: {}",
project_name
);
return Ok(Some(project_name.clone()));
}
}
Ok(None)
}
/// 从容器名称推断 Compose 项目名称
///
/// # Arguments
/// * `container_id` - 容器ID或hostname
///
/// # Returns
/// * `DockerResult<Option<String>>` - 项目名称或None
pub async fn get_compose_project_from_name(
&self,
container_id: &str,
) -> DockerResult<Option<String>> {
let inspect = self
.docker
.inspect_container(container_id, None::<InspectContainerOptions>)
.await
.map_err(|e| DockerError::ConnectionError(format!("failed to get container info: {}", e)))?;
// 从容器名称推断项目名称
if let Some(name) = inspect.name {
// 容器名称格式: /{project_name}-{service_name}-{number}
let clean_name = name.trim_start_matches('/');
if let Some(project_name) = clean_name.split('-').next() {
info!(
" container project: {}",
project_name
);
return Ok(Some(project_name.to_string()));
}
}
Ok(None)
}
/// 动态获取 Compose 项目名称(多种策略)
///
/// 尝试多种方法获取项目名称:
/// 1. 环境变量 COMPOSE_PROJECT_NAME
/// 2. 容器 labels
/// 3. 容器名称推断
///
/// # Arguments
/// * `container_id` - 容器ID或hostname (None表示使用当前容器)
///
/// # Returns
/// * `DockerResult<Option<String>>` - 项目名称或None
pub async fn get_dynamic_compose_project(
&self,
container_id: Option<&str>,
) -> DockerResult<Option<String>> {
// 方法1: 环境变量
if let Ok(project_name) = std::env::var("COMPOSE_PROJECT_NAME") {
info!(" got project: {}", project_name);
return Ok(Some(project_name));
}
let hostname = std::env::var("HOSTNAME").unwrap_or_else(|_| "self".to_string());
let cid = container_id.unwrap_or(&hostname);
// 方法2: 容器 labels
if let Some(project_name) = self.get_compose_project_from_labels(cid).await? {
return Ok(Some(project_name));
}
// 方法3: 容器名称推断
if let Some(project_name) = self.get_compose_project_from_name(cid).await? {
return Ok(Some(project_name));
}
warn!("Unable to get Docker Compose project");
Ok(None)
}
}

View File

@@ -0,0 +1,9 @@
//! Docker 网络管理模块
//!
//! 提供网络检测、Compose 项目名称获取等功能
pub mod detection;
pub mod utils;
pub use detection::*;
pub use utils::*;

View File

@@ -0,0 +1,65 @@
//! 网络工具函数
/// 根据项目名称构建网络名称
///
/// # Arguments
/// * `project_name` - Docker Compose 项目名称
///
/// # Returns
/// * `String` - 完整的网络名称
///
/// # Examples
/// ```
/// use docker_manager::network::build_network_name;
/// let network = build_network_name("rcoder");
/// // 返回: "rcoder_agent-network"
/// ```
pub fn build_network_name(project_name: &str) -> String {
format!("{}_{}", project_name, crate::RCODER_NETWORK_BASE_NAME)
}
/// 解析网络名称获取项目名称
///
/// # Arguments
/// * `network_name` - 完整的网络名称
///
/// # Returns
/// * `Option<String>` - 项目名称,如果无法解析返回 None
pub fn parse_project_from_network(network_name: &str) -> Option<String> {
if let Some(pos) = network_name.rfind('_') {
let suffix = &network_name[pos + 1..];
if suffix == crate::RCODER_NETWORK_BASE_NAME {
return Some(network_name[..pos].to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_network_name() {
let network = build_network_name("rcoder");
assert_eq!(network, "rcoder_agent-network");
let network = build_network_name("myproject");
assert_eq!(network, "myproject_agent-network");
}
#[test]
fn test_parse_project_from_network() {
assert_eq!(
parse_project_from_network("rcoder_agent-network"),
Some("rcoder".to_string())
);
assert_eq!(
parse_project_from_network("myproject_agent-network"),
Some("myproject".to_string())
);
assert_eq!(parse_project_from_network("invalid_network"), None);
}
}

View File

@@ -0,0 +1,9 @@
//! 路径解析模块
//!
//! 提供容器路径与宿主机路径之间的转换功能
pub mod resolver;
pub mod utils;
pub use resolver::*;
pub use utils::*;

View File

@@ -0,0 +1,417 @@
//! 路径解析器
//!
//! 从 rcoder/utils/host_path_resolver.rs 迁移
use crate::{ContainerSelfInspector, DockerError, DockerResult};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::{debug, info, warn};
/// 宿主机路径解析器
///
/// 用于将容器内的路径解析为宿主机路径
/// 支持容器嵌套场景(容器内运行容器)
#[derive(Clone)]
pub struct HostPathResolver {
/// 宿主机的项目工作空间根目录(默认工作空间,用于相对路径解析)
host_project_workspace: PathBuf,
/// 容器内的项目工作空间根目录(默认工作空间,用于相对路径解析)
container_project_workspace: PathBuf,
/// 所有挂载点缓存,按路径长度降序排列(最具体的路径优先匹配)
/// 格式: Vec<(container_path, host_path)>
all_mounts: Vec<(PathBuf, PathBuf)>,
/// 容器自检器(用于自动检测路径映射)
inspector: Option<Arc<ContainerSelfInspector>>,
}
impl HostPathResolver {
/// 创建新的路径解析器(自动检测配置)
///
/// 使用默认 Docker socket 路径自动检测路径映射
///
/// # Returns
/// * `DockerResult<Self>` - 路径解析器或错误
pub async fn new() -> DockerResult<Self> {
Self::new_with_docker_socket(None).await
}
/// 使用指定的 Docker socket 创建路径解析器
///
/// # Arguments
/// * `docker_socket_path` - Docker socket 路径None 表示使用默认路径)
///
/// # Returns
/// * `DockerResult<Self>` - 路径解析器或错误
pub async fn new_with_docker_socket(docker_socket_path: Option<String>) -> DockerResult<Self> {
debug!("startingcreated HostPathResolver");
// 创建容器自检器
let socket_path = docker_socket_path
.as_deref()
.unwrap_or("/var/run/docker.sock");
let inspector = Arc::new(
ContainerSelfInspector::new(socket_path)
.await
.map_err(|e| {
DockerError::ConfigurationError(format!("failed to create container self-inspector: {}", e))
})?,
);
// 获取所有挂载点信息
let mounts = inspector
.get_all_mounts()
.await
.map_err(|e| DockerError::ConfigurationError(format!("failed to get mount points: {}", e)))?;
if mounts.is_empty() {
return Err(DockerError::ConfigurationError(
"No mount points detected, check if running inside container and ensure docker-compose.yml has volume mounts configured".to_string()
));
}
// 🔍 缓存所有挂载点,按路径长度降序排列(最具体的路径优先匹配)
let mut all_mounts: Vec<(PathBuf, PathBuf)> = mounts
.iter()
.map(|(cp, hp)| (PathBuf::from(cp), PathBuf::from(hp)))
.collect();
// 按容器路径长度降序排列,确保最具体的路径优先匹配
all_mounts.sort_by(|a, b| b.0.as_os_str().len().cmp(&a.0.as_os_str().len()));
info!(
"[HostPathResolver] Cached {} mount points (sorted by path length descending):",
all_mounts.len()
);
for (idx, (cp, hp)) in all_mounts.iter().enumerate() {
debug!(" {}. {} -> {}", idx + 1, cp.display(), hp.display());
}
// 选择默认工作空间(用于相对路径解析)
// 优先选择 project_workspace 相关的挂载点
let default_workspace = all_mounts
.iter()
.find(|(container_path, _)| {
let path_str = container_path.to_string_lossy();
path_str.contains("computer-project-workspace")
|| path_str.contains("project_workspace")
})
.or_else(|| all_mounts.first())
// all_mounts 不为空(前面已检查),所以这里一定有值
.expect("all_mounts is not empty, checked above");
let (container_workspace, host_workspace) = default_workspace.clone();
info!(
"[HostPathResolver] Default workspace: {} (host) -> {} (container)",
host_workspace.display(),
container_workspace.display()
);
Ok(Self {
host_project_workspace: host_workspace,
container_project_workspace: container_workspace,
all_mounts,
inspector: Some(inspector),
})
}
/// 将容器内路径解析为宿主机路径
///
/// # Arguments
/// * `container_path` - 容器内的路径
///
/// # Returns
/// * `DockerResult<PathBuf>` - 宿主机路径或错误
///
/// # Examples
/// ```no_run
/// use docker_manager::path::HostPathResolver;
/// use std::path::Path;
///
/// # async fn example() -> docker_manager::DockerResult<()> {
/// let resolver = HostPathResolver::new().await?;
/// let host_path = resolver.resolve_to_host_path(
/// Path::new("/app/project_workspace/project-123/src")
/// )?;
/// println!("Host path: {}", host_path.display());
/// # Ok(())
/// # }
/// ```
pub fn resolve_to_host_path(&self, container_path: &Path) -> DockerResult<PathBuf> {
debug!(
"Resolving container path to host path: {}",
container_path.display()
);
// 如果路径是相对路径,先转换为绝对路径(相对于容器工作空间)
let container_path = if container_path.is_relative() {
self.container_project_workspace.join(container_path)
} else {
container_path.to_path_buf()
};
// 🆕 从缓存的挂载点中查找最具体的匹配
// all_mounts 已按路径长度降序排列,所以第一个匹配就是最具体的
for (mount_container, mount_host) in &self.all_mounts {
if container_path.starts_with(mount_container) {
// 找到匹配的挂载点
let relative_path = container_path
.strip_prefix(mount_container)
.unwrap_or(Path::new(""));
let host_path = mount_host.join(relative_path);
debug!(
"Resolved from mount point: {} -> {} (mount: {} -> {})",
container_path.display(),
host_path.display(),
mount_container.display(),
mount_host.display()
);
return Ok(host_path);
}
}
// 没有找到匹配的挂载点,返回错误
warn!(
"⚠️ unable to resolve path {}: no matching mount point found",
container_path.display()
);
Err(DockerError::ConfigurationError(format!(
"unable to resolve container path '{}': no matching mount point found. available mount points: {:?}",
container_path.display(),
self.all_mounts
.iter()
.map(|(c, h)| format!("{} -> {}", c.display(), h.display()))
.collect::<Vec<_>>()
)))
}
/// 获取宿主机工作空间根目录
pub fn host_workspace_base(&self) -> &Path {
&self.host_project_workspace
}
/// 获取容器工作空间根目录
pub fn container_workspace_base(&self) -> &Path {
&self.container_project_workspace
}
/// 验证路径解析器配置是否有效
///
/// # Returns
/// * `DockerResult<()>` - 验证成功或错误
pub fn validate(&self) -> DockerResult<()> {
if !self.host_project_workspace.is_absolute() {
return Err(DockerError::ConfigurationError(
"Host workspace must be an absolute path".to_string(),
));
}
if !self.container_project_workspace.is_absolute() {
return Err(DockerError::ConfigurationError(
"Container workspace must be an absolute path".to_string(),
));
}
Ok(())
}
/// 获取诊断信息(用于调试)
pub fn get_diagnostics(&self) -> String {
format!(
"HostPathResolver {{\n \
host_workspace: {},\n \
container_workspace: {},\n \
all_mounts: {:?},\n \
inspector: {}\n\
}}",
self.host_project_workspace.display(),
self.container_project_workspace.display(),
self.all_mounts
.iter()
.map(|(c, h)| format!("{} -> {}", c.display(), h.display()))
.collect::<Vec<_>>(),
if self.inspector.is_some() {
"enabled"
} else {
"disabled"
}
)
}
/// 检查 Docker 连接状态
///
/// # Returns
/// * `DockerResult<bool>` - 是否可以连接到 Docker
pub async fn check_docker_connection(&self) -> DockerResult<bool> {
if let Some(inspector) = &self.inspector {
inspector
.verify_docker_connection()
.await
.map(|_| true)
.map_err(|e| DockerError::ConnectionError(format!("Docker connection check failed: {}", e)))
} else {
Ok(false)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_resolver() -> HostPathResolver {
HostPathResolver {
host_project_workspace: PathBuf::from("/host/projects"),
container_project_workspace: PathBuf::from("/app/project_workspace"),
all_mounts: vec![
// 按路径长度降序排列
(
PathBuf::from("/app/project_workspace"),
PathBuf::from("/host/projects"),
),
(PathBuf::from("/app/logs"), PathBuf::from("/host/logs")),
(PathBuf::from("/app"), PathBuf::from("/host/app")),
],
inspector: None,
}
}
#[test]
fn test_resolve_relative_path() {
let resolver = create_test_resolver();
let container_path = Path::new("project-123/src/main.rs");
let host_path = resolver.resolve_to_host_path(container_path).unwrap();
assert_eq!(
host_path,
PathBuf::from("/host/projects/project-123/src/main.rs")
);
}
#[test]
fn test_resolve_absolute_path() {
let resolver = create_test_resolver();
let container_path = Path::new("/app/project_workspace/project-123/src/main.rs");
let host_path = resolver.resolve_to_host_path(container_path).unwrap();
assert_eq!(
host_path,
PathBuf::from("/host/projects/project-123/src/main.rs")
);
}
#[test]
fn test_resolve_logs_path() {
// 🆕 测试 /app/logs 路径解析(这是本次修复的关键测试)
let resolver = create_test_resolver();
let container_path = Path::new("/app/logs/container");
let host_path = resolver.resolve_to_host_path(container_path).unwrap();
assert_eq!(host_path, PathBuf::from("/host/logs/container"));
}
#[test]
fn test_resolve_logs_with_subdir() {
// 🆕 测试 /app/logs/container/subdir 路径解析
let resolver = create_test_resolver();
let container_path = Path::new("/app/logs/container/agent-runner-xxx-20251226");
let host_path = resolver.resolve_to_host_path(container_path).unwrap();
assert_eq!(
host_path,
PathBuf::from("/host/logs/container/agent-runner-xxx-20251226")
);
}
#[test]
fn test_resolve_outside_all_mounts() {
let resolver = create_test_resolver();
let container_path = Path::new("/etc/passwd");
let result = resolver.resolve_to_host_path(container_path);
// 不在任何挂载点内,应该返回错误
assert!(result.is_err());
}
#[test]
fn test_validate_success() {
let resolver = create_test_resolver();
assert!(resolver.validate().is_ok());
}
#[test]
fn test_validate_relative_host_path() {
let resolver = HostPathResolver {
host_project_workspace: PathBuf::from("host/projects"), // 相对路径
container_project_workspace: PathBuf::from("/app/project_workspace"),
all_mounts: vec![(
PathBuf::from("/app/project_workspace"),
PathBuf::from("host/projects"),
)],
inspector: None,
};
assert!(resolver.validate().is_err());
}
#[test]
fn test_workspace_base_accessors() {
let resolver = create_test_resolver();
assert_eq!(resolver.host_workspace_base(), Path::new("/host/projects"));
assert_eq!(
resolver.container_workspace_base(),
Path::new("/app/project_workspace")
);
}
#[test]
fn test_mount_order_priority() {
// 🆕 测试挂载点优先级:更具体的路径应该优先匹配
let resolver = HostPathResolver {
host_project_workspace: PathBuf::from("/host/projects"),
container_project_workspace: PathBuf::from("/app/project_workspace"),
all_mounts: vec![
// 最长路径优先
(
PathBuf::from("/app/project_workspace/special"),
PathBuf::from("/host/special"),
),
(
PathBuf::from("/app/project_workspace"),
PathBuf::from("/host/projects"),
),
(PathBuf::from("/app"), PathBuf::from("/host/app")),
],
inspector: None,
};
// /app/project_workspace/special/file 应该匹配第一个挂载点
let path1 = Path::new("/app/project_workspace/special/file.txt");
let result1 = resolver.resolve_to_host_path(path1).unwrap();
assert_eq!(result1, PathBuf::from("/host/special/file.txt"));
// /app/project_workspace/other/file 应该匹配第二个挂载点
let path2 = Path::new("/app/project_workspace/other/file.txt");
let result2 = resolver.resolve_to_host_path(path2).unwrap();
assert_eq!(result2, PathBuf::from("/host/projects/other/file.txt"));
}
// 注意:以下测试需要在 Docker 容器环境中运行
#[tokio::test]
#[ignore]
async fn test_new_in_container_environment() {
// 此测试仅在容器内有效
let result = HostPathResolver::new().await;
if std::env::var("HOSTNAME").is_ok() {
assert!(result.is_ok());
}
}
}

View File

@@ -0,0 +1,124 @@
//! 路径解析工具函数
//!
//! 提供便捷的路径解析接口
use crate::DockerResult;
use crate::path::HostPathResolver;
use std::path::{Path, PathBuf};
/// 便捷函数:将容器路径解析为宿主机路径
///
/// 自动创建 `HostPathResolver` 并执行路径解析
///
/// # Arguments
/// * `container_path` - 容器内的路径
///
/// # Returns
/// * `DockerResult<PathBuf>` - 宿主机路径或错误
///
/// # Examples
/// ```no_run
/// use docker_manager::path::resolve_container_path_to_host;
/// use std::path::Path;
///
/// # async fn example() -> docker_manager::DockerResult<()> {
/// let host_path = resolve_container_path_to_host(
/// Path::new("/app/project_workspace/project-123")
/// ).await?;
/// println!("Host path: {}", host_path.display());
/// # Ok(())
/// # }
/// ```
pub async fn resolve_container_path_to_host(container_path: &Path) -> DockerResult<PathBuf> {
let resolver = HostPathResolver::new().await?;
resolver.resolve_to_host_path(container_path)
}
/// 便捷函数:获取 HostPathResolver 实例
///
/// 使用默认配置创建路径解析器
///
/// # Returns
/// * `DockerResult<HostPathResolver>` - 路径解析器或错误
pub async fn get_host_path_resolver() -> DockerResult<HostPathResolver> {
HostPathResolver::new().await
}
/// 标准化路径(移除冗余的 `.` 和 `..` 组件)
///
/// # Arguments
/// * `path` - 要标准化的路径
///
/// # Returns
/// * `PathBuf` - 标准化后的路径
///
/// # Examples
/// ```
/// use docker_manager::path::normalize_path;
/// use std::path::Path;
///
/// let normalized = normalize_path(Path::new("/app/./project/../project/src"));
/// assert_eq!(normalized, Path::new("/app/project/src"));
/// ```
pub fn normalize_path(path: &Path) -> PathBuf {
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::CurDir => {
// 跳过 `.`
}
std::path::Component::ParentDir => {
// 处理 `..`
if !components.is_empty() {
components.pop();
}
}
other => {
components.push(other);
}
}
}
components.iter().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_path_current_dir() {
let path = Path::new("/app/./project/./src");
let normalized = normalize_path(path);
assert_eq!(normalized, Path::new("/app/project/src"));
}
#[test]
fn test_normalize_path_parent_dir() {
let path = Path::new("/app/project/../project/src");
let normalized = normalize_path(path);
assert_eq!(normalized, Path::new("/app/project/src"));
}
#[test]
fn test_normalize_path_mixed() {
let path = Path::new("/app/./project/../workspace/./src");
let normalized = normalize_path(path);
assert_eq!(normalized, Path::new("/app/workspace/src"));
}
#[test]
fn test_normalize_path_trailing_parent() {
let path = Path::new("/app/project/..");
let normalized = normalize_path(path);
assert_eq!(normalized, Path::new("/app"));
}
#[test]
fn test_normalize_path_relative() {
let path = Path::new("project/./src/../lib");
let normalized = normalize_path(path);
assert_eq!(normalized, Path::new("project/lib"));
}
}

View File

@@ -0,0 +1,219 @@
//! Port manager for dynamic port allocation
//!
//! Manages allocation and release of ports for Docker containers.
use dashmap::DashSet;
use std::ops::RangeInclusive;
use std::sync::Arc;
/// Port manager - responsible for dynamic port allocation and release
///
/// # Example
/// ```ignore
/// use docker_manager::port_manager::PortManager;
///
/// let manager = PortManager::new(3000, 65535);
/// let port = manager.allocate_port().unwrap();
/// manager.release_port(port);
/// ```
#[derive(Debug, Clone)]
pub struct PortManager {
/// Set of allocated ports
allocated_ports: Arc<DashSet<u16>>,
/// Available port range
port_range: RangeInclusive<u16>,
/// Whether to check actual port usage
check_actual_usage: bool,
}
impl PortManager {
/// Create a new port manager
///
/// # Arguments
/// - `min_port`: Minimum port number (inclusive)
/// - `max_port`: Maximum port number (inclusive)
///
/// # Example
/// ```ignore
/// let manager = PortManager::new(3000, 65535);
/// ```
pub fn new(min_port: u16, max_port: u16) -> Self {
Self {
allocated_ports: Arc::new(DashSet::new()),
port_range: min_port..=max_port,
check_actual_usage: true, // Enable actual port usage check by default
}
}
/// Create a new port manager without actual usage checking
///
/// This is faster but may allocate ports that are already in use by other processes.
pub fn new_without_check(min_port: u16, max_port: u16) -> Self {
Self {
allocated_ports: Arc::new(DashSet::new()),
port_range: min_port..=max_port,
check_actual_usage: false,
}
}
/// Check if a port is actually in use by the system
///
/// Uses socket binding to test if a port is available.
fn is_port_in_use(port: u16) -> bool {
use std::net::{Ipv4Addr, SocketAddrV4, TcpListener};
let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, port);
// Try to bind to the port
match TcpListener::bind(addr) {
Ok(_) => false, // Port is available
Err(_) => true, // Port is in use
}
}
/// Allocate an available port
///
/// Returns `Some(port)` if a port is available, `None` if all ports in range are allocated.
///
/// # Example
/// ```ignore
/// if let Some(port) = manager.allocate_port() {
/// println!("Allocated port: {}", port);
/// }
/// ```
pub fn allocate_port(&self) -> Option<u16> {
for port in self.port_range.clone() {
// Check if already allocated
if !self.allocated_ports.insert(port) {
continue; // Already allocated by us
}
// If actual usage check is enabled, verify the port is not in use
if self.check_actual_usage && Self::is_port_in_use(port) {
tracing::debug!("Port {} is in use by system, skipping", port);
self.allocated_ports.remove(&port);
continue;
}
tracing::debug!("Allocated port: {}", port);
return Some(port);
}
tracing::warn!("No available ports in range {:?}", self.port_range);
None
}
/// Release a port
///
/// # Example
/// ```ignore
/// manager.release_port(3000);
/// ```
pub fn release_port(&self, port: u16) {
if self.allocated_ports.remove(&port).is_some() {
tracing::debug!("Released port: {}", port);
}
}
/// Check if a port is allocated
///
/// # Example
/// ```ignore
/// if manager.is_allocated(3000) {
/// println!("Port 3000 is in use");
/// }
/// ```
pub fn is_allocated(&self, port: u16) -> bool {
self.allocated_ports.contains(&port)
}
/// Get the number of allocated ports
///
/// # Example
/// ```ignore
/// println!("Allocated ports: {}", manager.allocated_count());
/// ```
pub fn allocated_count(&self) -> usize {
self.allocated_ports.len()
}
/// Get the port range
///
/// # Example
/// ```ignore
/// let (min, max) = manager.port_range();
/// println!("Port range: {}-{}", min, max);
/// ```
pub fn port_range(&self) -> (u16, u16) {
(*self.port_range.start(), *self.port_range.end())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_port_allocation() {
let manager = PortManager::new(3000, 3010);
let port1 = manager.allocate_port().unwrap();
let port2 = manager.allocate_port().unwrap();
assert_ne!(port1, port2);
assert!(manager.is_allocated(port1));
assert!(manager.is_allocated(port2));
}
#[test]
fn test_port_release() {
let manager = PortManager::new(3000, 3010);
let port = manager.allocate_port().unwrap();
assert!(manager.is_allocated(port));
manager.release_port(port);
assert!(!manager.is_allocated(port));
}
#[test]
fn test_port_exhaustion() {
let manager = PortManager::new(3000, 3001); // Only 2 ports
let port1 = manager.allocate_port().unwrap();
let port2 = manager.allocate_port().unwrap();
let port3 = manager.allocate_port();
assert!(port3.is_none()); // No more ports available
}
#[test]
fn test_allocated_count() {
let manager = PortManager::new(3000, 3010);
assert_eq!(manager.allocated_count(), 0);
let _port1 = manager.allocate_port().unwrap();
let _port2 = manager.allocate_port().unwrap();
assert_eq!(manager.allocated_count(), 2);
manager.release_port(_port1);
assert_eq!(manager.allocated_count(), 1);
}
#[test]
fn test_port_range() {
let manager = PortManager::new(3000, 65535);
let (min, max) = manager.port_range();
assert_eq!(min, 3000);
assert_eq!(max, 65535);
}
#[test]
fn test_double_release() {
let manager = PortManager::new(3000, 3010);
let port = manager.allocate_port().unwrap();
// Releasing twice should not cause an error
manager.release_port(port);
manager.release_port(port);
}
}

View File

@@ -0,0 +1,259 @@
//! Docker runtime implementation
//!
//! This module provides `DockerRuntime` that wraps the existing `DockerManager`
//! and implements the `ContainerRuntime` trait.
use async_trait::async_trait;
use container_runtime_api::{
ContainerCreateParams, ContainerRuntime, ContainerRuntimeError, ContainerRuntimeResult,
ContainerRuntimeStatus, RemovedContainerInfo, RuntimeContainerInfo,
};
use moka::future::Cache;
use shared_types::{ContainerBasicInfo, ServiceType};
use std::sync::Arc;
use std::time::Duration;
use crate::DockerManager;
/// Docker runtime implementation wrapping DockerManager
pub struct DockerRuntime {
inner: Arc<DockerManager>,
/// TTL cache for list_containers result (15 seconds)
list_cache: Cache<(), Vec<RuntimeContainerInfo>>,
}
impl DockerRuntime {
/// Create a new DockerRuntime wrapping the given DockerManager
pub fn new(inner: Arc<DockerManager>) -> Self {
Self {
inner,
list_cache: Cache::builder()
.max_capacity(1)
.time_to_live(Duration::from_secs(15))
.build(),
}
}
}
#[async_trait]
impl ContainerRuntime for DockerRuntime {
async fn create_container(
&self,
params: ContainerCreateParams,
) -> ContainerRuntimeResult<ContainerBasicInfo> {
self.inner
.start_agent_container(params)
.await
.map_err(|e| ContainerRuntimeError::ContainerCreationError(e.to_string()))
}
async fn get_container_info(
&self,
project_id: &str,
) -> ContainerRuntimeResult<Option<ContainerBasicInfo>> {
self.inner
.get_agent_info(project_id)
.await
.map_err(|e| ContainerRuntimeError::ConnectionError(e.to_string()))
}
async fn get_container_info_by_identifier(
&self,
identifier: &str,
service_type: &ServiceType,
) -> ContainerRuntimeResult<Option<ContainerBasicInfo>> {
match service_type {
ServiceType::RCoder => self
.inner
.get_agent_info(identifier)
.await
.map_err(|e| ContainerRuntimeError::ConnectionError(e.to_string())),
// 使用 find_container 实时查询 Docker API 获取 IP
// 避免 get_user_container_info → get_agent_info → get_container_info 只查缓存
// 导致服务重启后缓存丢失返回 None
ServiceType::ComputerAgentRunner => {
let result = self.find_container(identifier, service_type).await?;
Ok(result.map(|pod| ContainerBasicInfo {
container_id: pod.container_id,
container_name: pod.container_name,
container_ip: pod.container_ip.clone(),
internal_port: shared_types::GRPC_DEFAULT_PORT,
external_port: 0,
project_id: identifier.to_string(),
status: String::from(pod.status),
created_at: pod.created_at,
service_url: format!(
"http://{}:{}",
pod.container_ip,
shared_types::GRPC_DEFAULT_PORT
),
}))
}
}
}
async fn find_container(
&self,
project_id: &str,
service_type: &ServiceType,
) -> ContainerRuntimeResult<Option<RuntimeContainerInfo>> {
let result = self
.inner
.find_project_container(project_id, service_type)
.await
.map_err(|e| ContainerRuntimeError::ConnectionError(e.to_string()))?;
Ok(result.map(|r| RuntimeContainerInfo {
container_id: r.container_id,
container_name: r.container_name,
container_ip: r.container_ip,
status: match r.status {
crate::types::ContainerStatus::Running => ContainerRuntimeStatus::Running,
crate::types::ContainerStatus::Stopped => ContainerRuntimeStatus::Failed,
crate::types::ContainerStatus::Creating => ContainerRuntimeStatus::Pending,
crate::types::ContainerStatus::Restarting => ContainerRuntimeStatus::Pending,
crate::types::ContainerStatus::Paused => {
ContainerRuntimeStatus::Unknown("paused".to_string())
}
crate::types::ContainerStatus::Dead => ContainerRuntimeStatus::Failed,
crate::types::ContainerStatus::Removing => ContainerRuntimeStatus::Failed,
crate::types::ContainerStatus::Exited => ContainerRuntimeStatus::Failed,
crate::types::ContainerStatus::Unknown(s) => {
ContainerRuntimeStatus::Unknown(s)
}
},
created_at: chrono::Utc::now(),
}))
}
async fn stop_container(&self, project_id: &str) -> ContainerRuntimeResult<()> {
self.inner
.stop_container(project_id)
.await
.map_err(|e| ContainerRuntimeError::ContainerStopError(e.to_string()))
}
async fn stop_container_by_identifier(
&self,
identifier: &str,
service_type: &ServiceType,
) -> ContainerRuntimeResult<()> {
match service_type {
ServiceType::RCoder => self
.inner
.stop_container(identifier)
.await
.map_err(|e| ContainerRuntimeError::ContainerStopError(e.to_string())),
ServiceType::ComputerAgentRunner => {
if let Some(container) = self
.inner
.find_user_container(identifier, service_type)
.await
.map_err(|e| ContainerRuntimeError::ContainerStopError(e.to_string()))?
{
self.inner
.stop_container_by_id(&container.container_id)
.await
.map_err(|e| ContainerRuntimeError::ContainerStopError(e.to_string()))?;
}
Ok(())
}
}
}
async fn is_container_running(&self, project_id: &str) -> ContainerRuntimeResult<bool> {
if let Some(info) = self.get_container_info(project_id).await? {
Ok(info.status == "running")
} else {
Ok(false)
}
}
async fn is_container_running_by_identifier(
&self,
identifier: &str,
service_type: &ServiceType,
) -> ContainerRuntimeResult<bool> {
Ok(self
.find_container(identifier, service_type)
.await?
.map(|c| c.status == ContainerRuntimeStatus::Running)
.unwrap_or(false))
}
async fn list_containers(&self) -> ContainerRuntimeResult<Vec<RuntimeContainerInfo>> {
// 尝试从缓存获取
if let Some(cached) = self.list_cache.get(&()).await {
return Ok(cached);
}
// 缓存未命中或过期fetch 并写入缓存
let result = self.fetch_containers().await?;
self.list_cache.insert((), result.clone()).await;
Ok(result)
}
async fn sync_states(&self) -> ContainerRuntimeResult<(u32, Vec<RemovedContainerInfo>)> {
self.inner
.sync_all_container_states()
.await
.map_err(|e| ContainerRuntimeError::DockerError(e.to_string()))
}
async fn cleanup_all(&self) -> ContainerRuntimeResult<()> {
self.inner
.cleanup_all_containers()
.await
.map_err(|e| ContainerRuntimeError::ConnectionError(e.to_string()))
}
async fn health_check(&self) -> ContainerRuntimeResult<()> {
self.inner
.get_docker_client()
.ping()
.await
.map_err(|e| {
ContainerRuntimeError::ConnectionError(format!("Docker ping failed: {}", e))
})?;
Ok(())
}
}
impl DockerRuntime {
/// Fetch containers from Docker API (used as cache loader)
async fn fetch_containers(&self) -> ContainerRuntimeResult<Vec<RuntimeContainerInfo>> {
let containers = self.inner.list_containers().await;
let mut result = Vec::with_capacity(containers.len());
for c in containers {
let container_ip = self
.inner
.get_container_connection_info(&c)
.await
.map_err(|e| ContainerRuntimeError::ConnectionError(e.to_string()))?
.unwrap_or_default();
result.push(RuntimeContainerInfo {
container_id: c.container_id,
container_name: c.container_name,
container_ip,
status: match c.status {
crate::types::ContainerStatus::Running => ContainerRuntimeStatus::Running,
crate::types::ContainerStatus::Stopped => ContainerRuntimeStatus::Failed,
crate::types::ContainerStatus::Creating => ContainerRuntimeStatus::Pending,
crate::types::ContainerStatus::Restarting => ContainerRuntimeStatus::Pending,
crate::types::ContainerStatus::Paused => {
ContainerRuntimeStatus::Unknown("paused".to_string())
}
crate::types::ContainerStatus::Dead => ContainerRuntimeStatus::Failed,
crate::types::ContainerStatus::Removing => ContainerRuntimeStatus::Failed,
crate::types::ContainerStatus::Exited => ContainerRuntimeStatus::Failed,
crate::types::ContainerStatus::Unknown(s) => {
ContainerRuntimeStatus::Unknown(s)
}
},
created_at: c.created_at,
});
}
Ok(result)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,109 @@
//! Runtime manager implementation
//!
//! This module provides `RuntimeManager` that selects and manages the appropriate
//! container runtime (Docker or Kubernetes) based on environment configuration.
use container_runtime_api::ContainerRuntime;
use std::sync::Arc;
use tokio::sync::OnceCell;
use tracing::{error, info};
use crate::runtime_selection::RuntimeType;
#[cfg(feature = "kubernetes")]
use super::KubernetesRuntime;
use super::DockerRuntime;
use crate::types::DockerManagerConfig;
/// Global runtime instance
static RUNTIME_INSTANCE: OnceCell<Arc<dyn ContainerRuntime>> = OnceCell::const_new();
/// Runtime manager that selects and manages the appropriate container runtime
pub struct RuntimeManager;
impl RuntimeManager {
/// Initialize the global runtime based on environment configuration
pub async fn init(
config: DockerManagerConfig,
) -> container_runtime_api::ContainerRuntimeResult<()> {
let runtime_type = RuntimeType::from_env();
let runtime: Arc<dyn ContainerRuntime> = match runtime_type {
RuntimeType::Docker => {
info!("[RUNTIME] Initializing Docker runtime");
let docker_manager = crate::DockerManager::new(config.clone())
.await
.map_err(|e| {
container_runtime_api::ContainerRuntimeError::ConnectionError(
e.to_string(),
)
})?;
Arc::new(DockerRuntime::new(Arc::new(docker_manager)))
}
#[cfg(feature = "kubernetes")]
RuntimeType::Kubernetes => {
info!("[RUNTIME] Initializing Kubernetes runtime");
let k8s_runtime = KubernetesRuntime::new(config.clone())
.await
.map_err(|e| {
container_runtime_api::ContainerRuntimeError::K8sError(e.to_string())
})?;
Arc::new(k8s_runtime)
}
#[cfg(not(feature = "kubernetes"))]
RuntimeType::Kubernetes => {
return Err(
container_runtime_api::ContainerRuntimeError::ConfigurationError(
"Kubernetes runtime requested but 'kubernetes' feature is not enabled."
.to_string(),
),
);
}
};
// Verify runtime health
runtime.health_check().await.map_err(|e| {
error!("[RUNTIME] Health check failed: {}", e);
e
})?;
RUNTIME_INSTANCE.set(runtime).map_err(|_| {
container_runtime_api::ContainerRuntimeError::ConfigurationError(
"Runtime already initialized".to_string(),
)
})?;
info!("[RUNTIME] Global runtime initialized successfully");
Ok(())
}
/// Get the global runtime instance
pub async fn get(
) -> container_runtime_api::ContainerRuntimeResult<Arc<dyn ContainerRuntime>> {
RUNTIME_INSTANCE.get().cloned().ok_or_else(|| {
container_runtime_api::ContainerRuntimeError::ConfigurationError(
"Runtime not initialized. Call RuntimeManager::init() first.".to_string(),
)
})
}
/// Check if Docker runtime is in use
pub fn is_docker() -> bool {
RuntimeType::from_env() == RuntimeType::Docker
}
/// Check if Kubernetes runtime is in use
#[cfg(feature = "kubernetes")]
pub fn is_kubernetes() -> bool {
RuntimeType::from_env() == RuntimeType::Kubernetes
}
#[cfg(not(feature = "kubernetes"))]
pub fn is_kubernetes() -> bool {
false
}
/// Get the current runtime type
pub fn runtime_type() -> RuntimeType {
RuntimeType::from_env()
}
}

View File

@@ -0,0 +1,13 @@
//! Runtime abstraction module
//!
//! This module provides container runtime abstraction to support both
//! Docker and Kubernetes backends.
pub mod docker_runtime;
pub mod kubernetes_runtime;
pub mod manager;
pub use docker_runtime::DockerRuntime;
#[cfg(feature = "kubernetes")]
pub use kubernetes_runtime::KubernetesRuntime;
pub use manager::RuntimeManager;

View File

@@ -0,0 +1,120 @@
//! Runtime type detection based on environment variables
//!
//! This module provides `RuntimeType` enum and detection logic to switch
//! between Docker and Kubernetes container runtimes at runtime.
use std::env;
use tracing::info;
/// Runtime type selection
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeType {
/// Docker runtime (default)
Docker,
/// Kubernetes runtime
Kubernetes,
}
impl RuntimeType {
/// Detect runtime type from environment variable
///
/// Supported values:
/// - "docker" or not set -> Docker runtime
/// - "kubernetes" or "k8s" -> Kubernetes runtime
pub fn from_env() -> Self {
match env::var("CONTAINER_RUNTIME").as_deref() {
Ok("kubernetes") | Ok("k8s") => {
info!("[RUNTIME] Using Kubernetes runtime (from CONTAINER_RUNTIME)");
RuntimeType::Kubernetes
}
Ok("docker") | Ok("") | Err(_) => {
info!("[RUNTIME] Using Docker runtime (from CONTAINER_RUNTIME or default)");
RuntimeType::Docker
}
Ok(other) => {
info!(
"[RUNTIME] Unknown CONTAINER_RUNTIME '{}', falling back to Docker",
other
);
RuntimeType::Docker
}
}
}
/// Check if Kubernetes runtime is enabled
#[cfg(feature = "kubernetes")]
pub fn is_kubernetes(&self) -> bool {
matches!(self, RuntimeType::Kubernetes)
}
#[cfg(not(feature = "kubernetes"))]
pub fn is_kubernetes(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_runtime_type_from_env_docker() {
// SAFETY: Test-only environment variable manipulation, runs in serial
unsafe {
env::set_var("CONTAINER_RUNTIME", "docker");
}
assert_eq!(RuntimeType::from_env(), RuntimeType::Docker);
// SAFETY: Test-only environment variable cleanup, runs in serial
unsafe {
env::remove_var("CONTAINER_RUNTIME");
}
}
#[test]
fn test_runtime_type_from_env_kubernetes() {
// SAFETY: Test-only environment variable manipulation, runs in serial
unsafe {
env::set_var("CONTAINER_RUNTIME", "kubernetes");
}
assert_eq!(RuntimeType::from_env(), RuntimeType::Kubernetes);
// SAFETY: Test-only environment variable cleanup, runs in serial
unsafe {
env::remove_var("CONTAINER_RUNTIME");
}
}
#[test]
fn test_runtime_type_from_env_k8s() {
// SAFETY: Test-only environment variable manipulation, runs in serial
unsafe {
env::set_var("CONTAINER_RUNTIME", "k8s");
}
assert_eq!(RuntimeType::from_env(), RuntimeType::Kubernetes);
// SAFETY: Test-only environment variable cleanup, runs in serial
unsafe {
env::remove_var("CONTAINER_RUNTIME");
}
}
#[test]
fn test_runtime_type_from_env_default() {
// SAFETY: Test-only environment variable cleanup, runs in serial
unsafe {
env::remove_var("CONTAINER_RUNTIME");
}
assert_eq!(RuntimeType::from_env(), RuntimeType::Docker);
}
#[test]
fn test_runtime_type_from_env_unknown() {
// SAFETY: Test-only environment variable manipulation, runs in serial
unsafe {
env::set_var("CONTAINER_RUNTIME", "unknown");
}
assert_eq!(RuntimeType::from_env(), RuntimeType::Docker);
// SAFETY: Test-only environment variable cleanup, runs in serial
unsafe {
env::remove_var("CONTAINER_RUNTIME");
}
}
}

View File

@@ -0,0 +1,709 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
/// Docker 容器配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerContainerConfig {
/// 项目 ID
pub project_id: String,
/// Docker 镜像
pub image: String,
/// 容器名称前缀
pub name_prefix: String,
/// 主机路径映射
pub host_path: String,
/// 容器内路径
pub container_path: String,
/// 工作目录
pub work_dir: String,
/// 环境变量
pub env_vars: HashMap<String, String>,
/// 端口映射
pub port_bindings: HashMap<String, String>,
/// 网络模式
pub network_mode: String,
/// 自动删除
pub auto_remove: bool,
/// 资源限制
pub resource_limits: Option<ResourceLimits>,
/// 额外的挂载点
pub extra_mounts: Vec<MountPoint>,
/// 启动命令
pub command: Option<Vec<String>>,
/// 入口点 (覆盖镜像默认入口点)
pub entrypoint: Option<Vec<String>>,
/// 网络名称 (可选,如果不指定则使用默认的 RCODER_NETWORK_NAME)
pub network_name: Option<String>,
// === 新增字段 (隔离类型支持) ===
/// 容器唯一标识(可选),用于容器复用
/// 若传值,则使用此 ID 作为容器标识,优先级高于 project_id/user_id
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pod_id: Option<String>,
/// 租户 ID可选用于多租户场景下的数据隔离
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tenant_id: Option<String>,
/// 空间 ID可选用于区分租户下的不同空间
#[serde(default, skip_serializing_if = "Option::is_none")]
pub space_id: Option<String>,
/// 隔离类型可选控制容器共享粒度tenant/space/project
#[serde(default, skip_serializing_if = "Option::is_none")]
pub isolation_type: Option<String>,
}
/// 挂载点配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MountPoint {
/// 主机路径
pub host_path: String,
/// 容器内路径
pub container_path: String,
/// 是否只读
pub read_only: bool,
}
/// 资源限制配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceLimits {
/// 内存限制 (字节,支持浮点数)
pub memory_limit: Option<f64>,
/// CPU 限制
pub cpu_limit: Option<f64>,
/// 交换空间限制 (字节,支持浮点数)
pub swap_limit: Option<f64>,
}
impl DockerContainerConfig {
/// 为指定服务类型创建配置
///
/// 使用服务类型动态获取容器名称前缀,避免硬编码
///
/// # Arguments
///
/// * `service_type` - 服务类型RCoder 或 ComputerAgentRunner
///
/// # Returns
///
/// 返回配置了正确容器前缀的 DockerContainerConfig
///
/// # Examples
///
/// ```
/// use docker_manager::DockerContainerConfig;
/// use shared_types::ServiceType;
///
/// let config = DockerContainerConfig::new_for_service(ServiceType::RCoder);
/// assert_eq!(config.name_prefix, "rcoder-agent");
///
/// let config = DockerContainerConfig::new_for_service(ServiceType::ComputerAgentRunner);
/// assert_eq!(config.name_prefix, "computer-agent-runner");
/// ```
pub fn new_for_service(service_type: shared_types::ServiceType) -> Self {
Self {
project_id: String::new(),
image: crate::default_docker_image(),
name_prefix: service_type.container_prefix().to_string(), // 🔧 动态获取
host_path: String::new(),
container_path: crate::DEFAULT_WORK_DIR.to_string(),
work_dir: crate::DEFAULT_WORK_DIR.to_string(),
env_vars: HashMap::new(),
port_bindings: HashMap::new(),
network_mode: crate::DEFAULT_NETWORK_MODE.to_string(),
auto_remove: false,
resource_limits: None,
extra_mounts: Vec::new(),
command: None,
entrypoint: None,
network_name: None,
// 新增字段(隔离类型支持)
pod_id: None,
tenant_id: None,
space_id: None,
isolation_type: None,
}
}
}
impl Default for DockerContainerConfig {
fn default() -> Self {
// 默认使用 RCoder 服务
Self::new_for_service(shared_types::ServiceType::RCoder)
}
}
/// 容器实时查询结果
///
/// 用于 `find_project_container` 和 `find_user_container` 的返回值,
/// 提供容器的基本实时信息。
#[derive(Debug, Clone)]
pub struct ContainerQueryResult {
/// 容器 ID
pub container_id: String,
/// 容器名称
pub container_name: String,
/// 容器状态
pub status: ContainerStatus,
/// 是否正在运行
pub is_running: bool,
/// 容器 IP 地址(用于 gRPC 健康检查)
pub container_ip: String,
}
/// 容器实时查询结果的 Arc 包装(用于缓存)
pub type ContainerQueryResultArc = Arc<ContainerQueryResult>;
impl ContainerQueryResult {
/// 创建新的查询结果
pub fn new(
container_id: String,
container_name: String,
status: ContainerStatus,
is_running: bool,
container_ip: String,
) -> Self {
Self {
container_id,
container_name,
status,
is_running,
container_ip,
}
}
/// 从元组创建(用于兼容旧代码)
pub fn from_tuple(tuple: (String, String, ContainerStatus, bool)) -> Self {
Self {
container_id: tuple.0,
container_name: tuple.1,
status: tuple.2,
is_running: tuple.3,
container_ip: String::new(), // 默认为空,需要后续更新
}
}
/// 创建带有 IP 地址的查询结果(完整构造)
pub fn with_ip(
container_id: String,
container_name: String,
status: ContainerStatus,
is_running: bool,
container_ip: String,
) -> Self {
Self {
container_id,
container_name,
status,
is_running,
container_ip,
}
}
/// 转换为元组(用于兼容旧代码)
pub fn to_tuple(&self) -> (String, String, ContainerStatus, bool) {
(
self.container_id.clone(),
self.container_name.clone(),
self.status.clone(),
self.is_running,
)
}
}
/// Docker 容器信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerContainerInfo {
/// 容器 ID
pub container_id: String,
/// 容器名称
pub container_name: String,
/// 项目 IDRCoder 模式的主键)
pub project_id: String,
/// 用户 IDComputerAgentRunner 模式的主键,可选)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
/// 服务类型RCoder 或 ComputerAgentRunner
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service_type: Option<shared_types::ServiceType>,
/// 镜像名称
pub image: String,
/// 状态
pub status: ContainerStatus,
/// 创建时间
pub created_at: DateTime<Utc>,
/// 启动时间
pub started_at: Option<DateTime<Utc>>,
/// 主机路径
pub host_path: String,
/// 容器内路径
pub container_path: String,
/// 端口映射
pub port_bindings: HashMap<String, String>,
/// 分配的端口号
pub assigned_port: u16,
/// 健康检查状态
pub health_status: Option<String>,
/// 🆕 服务层健康状态gRPC/HTTP 检查结果)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service_health: Option<crate::health::ServiceHealthStatus>,
/// 内部服务端口
pub internal_port: u16,
/// 网络名称
pub network_name: String,
}
impl DockerContainerInfo {
/// 创建基础容器信息(仅必填字段,其余使用默认值)
pub fn new(
container_id: String,
container_name: String,
project_id: String,
image: String,
) -> Self {
Self {
container_id,
container_name,
project_id,
user_id: None,
service_type: None,
image,
status: ContainerStatus::Running,
created_at: chrono::Utc::now(),
started_at: Some(chrono::Utc::now()),
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: shared_types::GRPC_DEFAULT_PORT,
network_name: String::new(),
}
}
/// 获取容器的业务主键
///
/// 根据 `service_type` 返回正确的标识符:
/// - **RCoder**: 返回 `project_id`
/// - **ComputerAgentRunner**: 返回 `user_id`(如果有),否则回退到 `project_id`
///
/// # Returns
/// 容器的业务标识符
pub fn container_key(&self) -> &str {
match self.service_type {
Some(shared_types::ServiceType::ComputerAgentRunner) => {
// ComputerAgentRunner 模式优先使用 user_id
self.user_id.as_deref().unwrap_or(&self.project_id)
}
Some(shared_types::ServiceType::RCoder) => {
// RCoder 模式使用 project_id
&self.project_id
}
_ => {
// 未知类型使用 project_id
&self.project_id
}
}
}
/// 判断是否为 ComputerAgentRunner 容器
pub fn is_computer_agent(&self) -> bool {
matches!(
self.service_type,
Some(shared_types::ServiceType::ComputerAgentRunner)
)
}
}
/// 容器基本信息使用shared_types中的定义
pub type ContainerBasicInfo = shared_types::ContainerBasicInfo;
/// 容器状态
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ContainerStatus {
/// 创建中
Creating,
/// 运行中
Running,
/// 已停止
Stopped,
/// 已暂停
Paused,
/// 重启中
Restarting,
/// 移除中
Removing,
/// 已退出
Exited,
/// 已死亡
Dead,
/// 未知状态
Unknown(String),
}
impl From<String> for ContainerStatus {
fn from(status: String) -> Self {
match status.to_lowercase().as_str() {
"created" => ContainerStatus::Creating,
"running" => ContainerStatus::Running,
"stopped" => ContainerStatus::Stopped,
"paused" => ContainerStatus::Paused,
"restarting" => ContainerStatus::Restarting,
"removing" => ContainerStatus::Removing,
"exited" => ContainerStatus::Exited,
"dead" => ContainerStatus::Dead,
_ => ContainerStatus::Unknown(status),
}
}
}
impl ToString for ContainerStatus {
fn to_string(&self) -> String {
match self {
ContainerStatus::Creating => "created".to_string(),
ContainerStatus::Running => "running".to_string(),
ContainerStatus::Stopped => "stopped".to_string(),
ContainerStatus::Paused => "paused".to_string(),
ContainerStatus::Restarting => "restarting".to_string(),
ContainerStatus::Removing => "removing".to_string(),
ContainerStatus::Exited => "exited".to_string(),
ContainerStatus::Dead => "dead".to_string(),
ContainerStatus::Unknown(s) => s.clone(),
}
}
}
/// Docker 管理器配置
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerManagerConfig {
/// Docker 守护进程地址
pub docker_host: Option<String>,
/// 默认镜像
pub default_image: String,
/// 默认平台
pub default_platform: String,
/// 默认网络模式
pub default_network_mode: String,
/// 默认工作目录
pub default_work_dir: String,
/// 是否启用自动清理
pub auto_cleanup: bool,
/// 容器存活时间 (秒)
pub container_ttl_seconds: Option<u64>,
/// 多镜像配置(从 rcoder 配置传递,始终有值)
pub multi_image_config: shared_types::MultiImageConfig,
/// 网络基础名称(不含 project name 前缀)
/// Docker Compose 会自动添加 project name 前缀,实际网络名称为 {project_name}_{network_base_name}
/// 例如: network_base_name="agent-network" 时,实际网络为 "rcoder_agent-network"
pub network_base_name: String,
/// 🔧 Docker API 调用超时时间(秒)
/// 用于大多数 Docker API 调用的超时保护
#[serde(default = "default_api_timeout")]
pub api_timeout_seconds: u64,
/// 🔧 快速操作超时时间(秒)
/// 用于状态查询等轻量级操作的超时保护
#[serde(default = "default_api_timeout_quick")]
pub api_timeout_quick_seconds: u64,
/// 🔧 状态缓存 TTL
/// 用于缓存容器状态信息container_id, container_name, status, is_running
#[serde(default = "default_cache_status_ttl")]
pub cache_status_ttl_seconds: u64,
/// 🔧 网络缓存 TTL
/// 用于缓存容器网络信息network_name -> ip_address
#[serde(default = "default_cache_network_ttl")]
pub cache_network_ttl_seconds: u64,
/// 🔧 缓存最大容量
/// 用于 DockerApiCache 的 status_cache 和 network_cache
#[serde(default = "default_cache_max_capacity")]
pub cache_max_capacity: u64,
}
/// 默认 API 超时时间10秒
fn default_api_timeout() -> u64 {
10
}
/// 默认快速操作超时时间5秒
fn default_api_timeout_quick() -> u64 {
5
}
/// 默认状态缓存 TTL10秒
fn default_cache_status_ttl() -> u64 {
10
}
/// 默认网络缓存 TTL15秒
fn default_cache_network_ttl() -> u64 {
15
}
/// 默认缓存最大容量10000
fn default_cache_max_capacity() -> u64 {
10000
}
/// Docker 配置(从 rcoder 配置传递)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerConfig {
/// Docker 镜像名称(根据架构自动选择)
pub image: Option<String>,
/// ARM64 架构的 Docker 镜像
pub arm64_image: Option<String>,
/// AMD64 架构的 Docker 镜像
pub amd64_image: Option<String>,
/// 默认回退镜像(当无法检测架构或架构不匹配时使用)
pub default_image: Option<String>,
/// 默认网络模式
pub network_mode: Option<String>,
/// 默认工作目录
pub work_dir: Option<String>,
/// 是否启用自动清理
pub auto_cleanup: Option<bool>,
/// 容器存活时间(秒)
pub container_ttl_seconds: Option<u64>,
}
impl Default for DockerManagerConfig {
fn default() -> Self {
Self {
docker_host: None, // 使用默认的 Docker socket
default_image: crate::default_docker_image(),
default_platform: crate::default_platform(),
default_network_mode: crate::DEFAULT_NETWORK_MODE.to_string(),
default_work_dir: crate::DEFAULT_WORK_DIR.to_string(),
auto_cleanup: true,
container_ttl_seconds: Some(3600), // 1小时
multi_image_config: shared_types::create_default_multi_image_config(), // 默认多镜像配置
network_base_name: crate::RCODER_NETWORK_BASE_NAME.to_string(), // 默认网络基础名称
api_timeout_seconds: default_api_timeout(), // 默认 10 秒
api_timeout_quick_seconds: default_api_timeout_quick(), // 默认 5 秒
cache_status_ttl_seconds: default_cache_status_ttl(), // 默认 10 秒
cache_network_ttl_seconds: default_cache_network_ttl(), // 默认 15 秒
cache_max_capacity: default_cache_max_capacity(), // 默认 10000
}
}
}
/// 容器清理结果统计
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CleanupResult {
/// 找到的容器数量
pub total_found: usize,
/// 成功删除的容器数量
pub successfully_removed: usize,
/// 删除失败的容器数量
pub failed_removals: usize,
/// 跳过的运行中容器数量(仅在非强制删除时)
pub skipped_running: usize,
/// 被删除的容器ID列表
pub removed_container_ids: Vec<String>,
/// 失败的容器及错误信息
pub failed_removals_details: Vec<ContainerRemovalFailure>,
/// 清理操作耗时(毫秒)
pub duration_ms: u64,
}
impl CleanupResult {
/// 是否完全成功(没有失败)
pub fn is_complete_success(&self) -> bool {
self.failed_removals == 0
}
/// 是否有任何成功删除的容器
pub fn has_removals(&self) -> bool {
self.successfully_removed > 0
}
/// 获取成功率百分比
pub fn success_rate(&self) -> f64 {
if self.total_found == 0 {
100.0
} else {
(self.successfully_removed as f64 / self.total_found as f64) * 100.0
}
}
}
/// 容器删除失败信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerRemovalFailure {
/// 容器ID
pub container_id: String,
/// 容器名称
pub container_name: String,
/// 失败原因
pub error_message: String,
}
/// 容器过滤条件
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ContainerFilter {
/// 按名称模式过滤
NamePattern(String),
/// 按状态过滤
Status(Vec<ContainerStatus>),
/// 按标签过滤
Label(String, String),
/// 组合过滤条件AND逻辑
And(Vec<ContainerFilter>),
/// 组合过滤条件OR逻辑
Or(Vec<ContainerFilter>),
}
impl ContainerFilter {
/// 创建名称模式过滤器
pub fn name_pattern(pattern: impl Into<String>) -> Self {
ContainerFilter::NamePattern(pattern.into())
}
/// 创建状态过滤器
pub fn status(statuses: Vec<ContainerStatus>) -> Self {
ContainerFilter::Status(statuses)
}
/// 创建标签过滤器
pub fn label(key: impl Into<String>, value: impl Into<String>) -> Self {
ContainerFilter::Label(key.into(), value.into())
}
/// 创建AND组合过滤器
pub fn and(filters: Vec<ContainerFilter>) -> Self {
ContainerFilter::And(filters)
}
/// 创建OR组合过滤器
pub fn or(filters: Vec<ContainerFilter>) -> Self {
ContainerFilter::Or(filters)
}
/// 检查容器是否匹配过滤条件
pub fn matches(&self, container: &bollard::models::ContainerSummary) -> bool {
match self {
ContainerFilter::NamePattern(pattern) => {
if let Some(names) = &container.names {
for name in names {
// Docker 容器名称通常以 '/' 开头,需要去掉
let clean_name = name.trim_start_matches('/');
if Self::matches_pattern(clean_name, pattern) {
return true;
}
}
}
false
}
ContainerFilter::Status(statuses) => {
if let Some(state) = &container.state {
// 转换为字符串进行比较
let state_str = match state {
bollard::models::ContainerSummaryStateEnum::RUNNING => "running",
bollard::models::ContainerSummaryStateEnum::EXITED => "exited",
bollard::models::ContainerSummaryStateEnum::CREATED => "created",
bollard::models::ContainerSummaryStateEnum::PAUSED => "paused",
bollard::models::ContainerSummaryStateEnum::RESTARTING => "restarting",
bollard::models::ContainerSummaryStateEnum::REMOVING => "removing",
bollard::models::ContainerSummaryStateEnum::DEAD => "dead",
bollard::models::ContainerSummaryStateEnum::STOPPING => "stopping",
bollard::models::ContainerSummaryStateEnum::EMPTY => "unknown",
};
statuses.iter().any(|s| s.to_string() == state_str)
} else {
false
}
}
ContainerFilter::Label(key, value) => {
if let Some(labels) = &container.labels {
labels.get(key.as_str()) == Some(value)
} else {
false
}
}
ContainerFilter::And(filters) => filters.iter().all(|f| f.matches(container)),
ContainerFilter::Or(filters) => filters.iter().any(|f| f.matches(container)),
}
}
/// 简单的模式匹配(支持 * 通配符)
fn matches_pattern(text: &str, pattern: &str) -> bool {
// 如果模式不包含通配符,直接比较
if !pattern.contains('*') {
return text == pattern;
}
// 简单的通配符匹配实现
Self::wildcard_match(text, pattern)
}
/// 通配符匹配实现
fn wildcard_match(text: &str, pattern: &str) -> bool {
let pattern_chars: Vec<char> = pattern.chars().collect();
let text_chars: Vec<char> = text.chars().collect();
let mut text_idx = 0;
let mut pattern_idx = 0;
let mut star_idx = -1isize;
let mut match_idx = 0;
while text_idx < text_chars.len() {
if pattern_idx < pattern_chars.len()
&& (pattern_chars[pattern_idx] == text_chars[text_idx]
|| pattern_chars[pattern_idx] == '?')
{
text_idx += 1;
pattern_idx += 1;
} else if pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' {
star_idx = pattern_idx as isize;
match_idx = text_idx as isize;
pattern_idx += 1;
} else if star_idx != -1 {
pattern_idx = (star_idx + 1) as usize;
match_idx += 1;
text_idx = match_idx as usize;
} else {
return false;
}
}
// 处理模式末尾的通配符
while pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' {
pattern_idx += 1;
}
pattern_idx == pattern_chars.len() && text_idx == text_chars.len()
}
}
/// 容器清理选项
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CleanupOptions {
/// 是否强制删除运行中的容器
pub force_remove_running: bool,
/// 是否等待容器优雅停止
pub wait_for_graceful_stop: bool,
/// 优雅停止超时时间(秒)
pub stop_timeout_seconds: u64,
/// 删除容器后是否同时清理相关卷
pub remove_associated_volumes: bool,
}
impl Default for CleanupOptions {
fn default() -> Self {
Self {
force_remove_running: false,
wait_for_graceful_stop: true,
stop_timeout_seconds: 30,
remove_associated_volumes: false,
}
}
}

View File

@@ -0,0 +1,107 @@
use super::DockerManagerConfig;
use tracing::debug;
/// Docker 工具函数
pub struct DockerUtils;
impl DockerUtils {
/// 自动检测当前系统架构并返回对应的 Docker 平台字符串
pub fn auto_detect_platform() -> String {
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
debug!("detecting platform: {} {}", os, arch);
match (os, arch) {
// macOS ARM64
("macos", "aarch64") => "linux/arm64",
// Linux ARM64
("linux", "aarch64") => "linux/arm64",
// macOS AMD64
("macos", "x86_64") => "linux/amd64",
// Linux AMD64
("linux", "x86_64") => "linux/amd64",
// Windows AMD64 (在 Windows 上运行 Docker Desktop)
("windows", "x86_64") => "linux/amd64",
// 其他 ARM64 变体
(_, "arm64") => "linux/arm64",
// 默认回退到 AMD64
_ => {
debug!("not supported {} {}, default to linux/amd64", os, arch);
"linux/amd64"
}
}
.to_string()
}
/// 获取最佳的平台配置(优先使用环境变量,否则自动检测)
pub fn get_optimal_platform() -> String {
// 优先使用环境变量
if let Ok(platform) = std::env::var("DOCKER_DEFAULT_PLATFORM") {
debug!(" using config platform: {}", platform);
return platform;
}
// 否则自动检测
let detected = Self::auto_detect_platform();
debug!(" auto detected: {}", detected);
detected
}
/// 生成容器名称:使用 project_id 而不是随机 UUID便于管理和调试
pub fn generate_container_name(prefix: &str, project_id: &str) -> String {
format!("{}-{}", prefix, project_id)
}
/// 从环境变量加载 Docker 配置
pub fn config_from_env() -> DockerManagerConfig {
let mut config = DockerManagerConfig::default();
if let Ok(host) = std::env::var("DOCKER_HOST") {
config.docker_host = Some(host);
}
if let Ok(image) = std::env::var("DEFAULT_DOCKER_IMAGE") {
config.default_image = image;
}
// 自动检测平台配置(优先使用环境变量,否则自动检测)
config.default_platform = Self::get_optimal_platform();
// 🔍 调试日志:打印自动检测结果
debug!("DockerUtils::config_from_env auto detect:");
debug!(" - default_platform: {}", config.default_platform);
debug!(" - default_image: {}", config.default_image);
if let Ok(network) = std::env::var("DOCKER_NETWORK_MODE") {
config.default_network_mode = network;
}
if let Ok(work_dir) = std::env::var("DOCKER_WORK_DIR") {
config.default_work_dir = work_dir;
}
if let Ok(auto_cleanup) = std::env::var("DOCKER_AUTO_CLEANUP") {
config.auto_cleanup = auto_cleanup.parse().unwrap_or(true);
}
if let Ok(ttl) = std::env::var("DOCKER_CONTAINER_TTL") {
match ttl.parse() {
Ok(seconds) => config.container_ttl_seconds = Some(seconds),
Err(e) => {
tracing::warn!(
"⚠️ [DOCKER] Failed to parse DOCKER_CONTAINER_TTL '{}': {}, using default value",
ttl,
e
);
}
}
}
if let Ok(network_base_name) = std::env::var("DOCKER_NETWORK_BASE_NAME") {
config.network_base_name = network_base_name;
}
config
}
}

View File

@@ -0,0 +1,131 @@
use chrono::Utc;
use docker_manager::{ContainerStateActor, ContainerStatus, DockerContainerInfo};
use std::collections::HashMap;
fn create_test_info(project_id: &str) -> DockerContainerInfo {
DockerContainerInfo {
container_id: format!("test_container_id_{}", project_id),
container_name: format!("test_container_name_{}", project_id),
project_id: project_id.to_string(),
user_id: None,
service_type: None,
image: "test_image".to_string(),
status: ContainerStatus::Running,
created_at: Utc::now(),
started_at: Some(Utc::now()),
host_path: "/tmp/host".to_string(),
container_path: "/app".to_string(),
port_bindings: HashMap::new(),
assigned_port: 8080,
health_status: Some("healthy".to_string()),
service_health: None,
internal_port: 8080,
network_name: "test_net".to_string(),
}
}
#[tokio::test]
async fn test_actor_workflow() {
// 1. Create Actor
let (actor, handle) = ContainerStateActor::new();
// 2. Spawn Actor
let actor_handle = tokio::spawn(actor.run());
// 3. Test: Initially empty
assert_eq!(handle.len().await, 0);
assert!(handle.is_empty().await);
// 4. Test: Insert
let info1 = create_test_info("p1");
handle.insert("p1".to_string(), info1.clone()).await;
assert_eq!(handle.len().await, 1);
assert!(!handle.is_empty().await);
assert!(handle.contains_key("p1").await);
// 5. Test: Get
let retrieved = handle.get("p1").await;
assert!(retrieved.is_some());
let r = retrieved.unwrap();
assert_eq!(r.container_id, info1.container_id);
assert_eq!(r.project_id, "p1");
// 6. Test: List & Keys
let info2 = create_test_info("p2");
handle.insert("p2".to_string(), info2).await;
let list = handle.list().await;
assert_eq!(list.len(), 2);
let keys = handle.keys().await;
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"p1".to_string()));
assert!(keys.contains(&"p2".to_string()));
// 7. Test: Remove
let removed = handle.remove("p1").await;
assert!(removed.is_some());
assert_eq!(removed.unwrap().project_id, "p1");
assert_eq!(handle.len().await, 1);
assert!(!handle.contains_key("p1").await);
assert!(handle.contains_key("p2").await);
// 8. Test: Remove non-existent
let removed_none = handle.remove("non_existent").await;
assert!(removed_none.is_none());
// Clean up (Actor will stop when handle is dropped, but we can just let test finish)
drop(handle);
let _ = actor_handle.await;
}
#[tokio::test]
async fn test_actor_update() {
let (actor, handle) = ContainerStateActor::new();
tokio::spawn(actor.run());
let mut info = create_test_info("p1");
handle.insert("p1".to_string(), info.clone()).await;
// Modify info
info.status = ContainerStatus::Stopped;
// Update if exists
let updated = handle.update_if_exists("p1", info.clone()).await;
assert!(updated);
let retrieved = handle.get("p1").await.unwrap();
assert_eq!(retrieved.status, ContainerStatus::Stopped);
// Update non-existent
let updated_fail = handle.update_if_exists("p2", info).await;
assert!(!updated_fail);
}
#[tokio::test]
async fn test_remove_if_container_id() {
let (actor, handle) = ContainerStateActor::new();
tokio::spawn(actor.run());
let info = create_test_info("p1");
// info.container_id is "test_container_id_p1"
handle.insert("p1".to_string(), info.clone()).await;
// 1. Try remove with WRONG container_id
let removed = handle.remove_if_container_id("p1", "wrong_id").await;
assert!(removed.is_none());
// Verify still exists
assert!(handle.get("p1").await.is_some());
// 2. Try remove with CORRECT container_id
let removed = handle
.remove_if_container_id("p1", "test_container_id_p1")
.await;
assert!(removed.is_some());
// Verify removed
assert!(handle.get("p1").await.is_none());
}