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

303 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 容器自检测器
//!
//! 用于在容器内部通过 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 文件内容进行测试
// 由于测试环境不在容器内,这个测试可能需要跳过
}
}