添加qiming-rcoder模块
This commit is contained in:
61
qiming-rcoder/crates/docker_manager/Cargo.toml
Normal file
61
qiming-rcoder/crates/docker_manager/Cargo.toml
Normal 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 = []
|
||||
234
qiming-rcoder/crates/docker_manager/README.md
Normal file
234
qiming-rcoder/crates/docker_manager/README.md
Normal 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
|
||||
```
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! 容器构建器模块
|
||||
//!
|
||||
//! 提供 Builder 模式的容器配置构建和挂载点处理功能
|
||||
|
||||
pub mod config_builder;
|
||||
pub mod mount_processor;
|
||||
|
||||
pub use config_builder::*;
|
||||
pub use mount_processor::*;
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 文件内容进行测试
|
||||
// 由于测试环境不在容器内,这个测试可能需要跳过
|
||||
}
|
||||
}
|
||||
364
qiming-rcoder/crates/docker_manager/src/container_state_actor.rs
Normal file
364
qiming-rcoder/crates/docker_manager/src/container_state_actor.rs
Normal 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 succeeded,false 表示 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.
|
||||
600
qiming-rcoder/crates/docker_manager/src/container_stop.rs
Normal file
600
qiming-rcoder/crates/docker_manager/src/container_stop.rs
Normal 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)
|
||||
}
|
||||
125
qiming-rcoder/crates/docker_manager/src/health/http_health.rs
Normal file
125
qiming-rcoder/crates/docker_manager/src/health/http_health.rs
Normal 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
|
||||
}
|
||||
7
qiming-rcoder/crates/docker_manager/src/health/mod.rs
Normal file
7
qiming-rcoder/crates/docker_manager/src/health/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
//! 容器健康检查模块
|
||||
|
||||
pub mod http_health;
|
||||
pub mod service_health;
|
||||
|
||||
pub use http_health::*;
|
||||
pub use service_health::*;
|
||||
264
qiming-rcoder/crates/docker_manager/src/health/service_health.rs
Normal file
264
qiming-rcoder/crates/docker_manager/src/health/service_health.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
155
qiming-rcoder/crates/docker_manager/src/image_selector.rs
Normal file
155
qiming-rcoder/crates/docker_manager/src/image_selector.rs
Normal 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
|
||||
)))
|
||||
}
|
||||
}
|
||||
303
qiming-rcoder/crates/docker_manager/src/lib.rs
Normal file
303
qiming-rcoder/crates/docker_manager/src/lib.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
3448
qiming-rcoder/crates/docker_manager/src/manager.rs
Normal file
3448
qiming-rcoder/crates/docker_manager/src/manager.rs
Normal file
File diff suppressed because it is too large
Load Diff
185
qiming-rcoder/crates/docker_manager/src/network/detection.rs
Normal file
185
qiming-rcoder/crates/docker_manager/src/network/detection.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
9
qiming-rcoder/crates/docker_manager/src/network/mod.rs
Normal file
9
qiming-rcoder/crates/docker_manager/src/network/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! Docker 网络管理模块
|
||||
//!
|
||||
//! 提供网络检测、Compose 项目名称获取等功能
|
||||
|
||||
pub mod detection;
|
||||
pub mod utils;
|
||||
|
||||
pub use detection::*;
|
||||
pub use utils::*;
|
||||
65
qiming-rcoder/crates/docker_manager/src/network/utils.rs
Normal file
65
qiming-rcoder/crates/docker_manager/src/network/utils.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
9
qiming-rcoder/crates/docker_manager/src/path/mod.rs
Normal file
9
qiming-rcoder/crates/docker_manager/src/path/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! 路径解析模块
|
||||
//!
|
||||
//! 提供容器路径与宿主机路径之间的转换功能
|
||||
|
||||
pub mod resolver;
|
||||
pub mod utils;
|
||||
|
||||
pub use resolver::*;
|
||||
pub use utils::*;
|
||||
417
qiming-rcoder/crates/docker_manager/src/path/resolver.rs
Normal file
417
qiming-rcoder/crates/docker_manager/src/path/resolver.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
124
qiming-rcoder/crates/docker_manager/src/path/utils.rs
Normal file
124
qiming-rcoder/crates/docker_manager/src/path/utils.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
219
qiming-rcoder/crates/docker_manager/src/port_manager.rs
Normal file
219
qiming-rcoder/crates/docker_manager/src/port_manager.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
109
qiming-rcoder/crates/docker_manager/src/runtime/manager.rs
Normal file
109
qiming-rcoder/crates/docker_manager/src/runtime/manager.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
13
qiming-rcoder/crates/docker_manager/src/runtime/mod.rs
Normal file
13
qiming-rcoder/crates/docker_manager/src/runtime/mod.rs
Normal 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;
|
||||
120
qiming-rcoder/crates/docker_manager/src/runtime_selection.rs
Normal file
120
qiming-rcoder/crates/docker_manager/src/runtime_selection.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
709
qiming-rcoder/crates/docker_manager/src/types.rs
Normal file
709
qiming-rcoder/crates/docker_manager/src/types.rs
Normal 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,
|
||||
/// 项目 ID(RCoder 模式的主键)
|
||||
pub project_id: String,
|
||||
/// 用户 ID(ComputerAgentRunner 模式的主键,可选)
|
||||
#[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
|
||||
}
|
||||
|
||||
/// 默认状态缓存 TTL(10秒)
|
||||
fn default_cache_status_ttl() -> u64 {
|
||||
10
|
||||
}
|
||||
|
||||
/// 默认网络缓存 TTL(15秒)
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
107
qiming-rcoder/crates/docker_manager/src/utils.rs
Normal file
107
qiming-rcoder/crates/docker_manager/src/utils.rs
Normal 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
|
||||
}
|
||||
}
|
||||
131
qiming-rcoder/crates/docker_manager/tests/actor_test.rs
Normal file
131
qiming-rcoder/crates/docker_manager/tests/actor_test.rs
Normal 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());
|
||||
}
|
||||
Reference in New Issue
Block a user