use bollard::container::{ Config, CreateContainerOptions, ListContainersOptions, RemoveContainerOptions, StartContainerOptions, StopContainerOptions, }; use bollard::models::{ContainerSummary, HostConfig, Mount, MountTypeEnum}; use std::collections::HashMap; use super::client::get_docker; use crate::models::{container_config, ContainerInfo, Project}; pub async fn find_existing_container(project: &Project) -> Result, String> { let docker = get_docker()?; let container_name = project.container_name(); let filters: HashMap> = HashMap::from([ ("name".to_string(), vec![container_name.clone()]), ]); let containers: Vec = docker .list_containers(Some(ListContainersOptions { all: true, filters, ..Default::default() })) .await .map_err(|e| format!("Failed to list containers: {}", e))?; // Match exact name (Docker prepends /) let expected = format!("/{}", container_name); for c in &containers { if let Some(names) = &c.names { if names.iter().any(|n| n == &expected) { return Ok(c.id.clone()); } } } Ok(None) } pub async fn create_container( project: &Project, api_key: Option<&str>, docker_socket_path: &str, ) -> Result { let docker = get_docker()?; let container_name = project.container_name(); let image = container_config::full_image_name(); let mut env_vars: Vec = Vec::new(); if let Some(key) = api_key { env_vars.push(format!("ANTHROPIC_API_KEY={}", key)); } if let Some(ref token) = project.git_token { env_vars.push(format!("GIT_TOKEN={}", token)); } if let Some(ref name) = project.git_user_name { env_vars.push(format!("GIT_USER_NAME={}", name)); } if let Some(ref email) = project.git_user_email { env_vars.push(format!("GIT_USER_EMAIL={}", email)); } let mut mounts = vec![ // Project directory -> /workspace Mount { target: Some("/workspace".to_string()), source: Some(project.path.clone()), typ: Some(MountTypeEnum::BIND), read_only: Some(false), ..Default::default() }, // Named volume for claude config persistence Mount { target: Some("/home/claude/.claude".to_string()), source: Some(format!("triple-c-claude-config-{}", project.id)), typ: Some(MountTypeEnum::VOLUME), read_only: Some(false), ..Default::default() }, ]; // SSH keys mount (read-only) if let Some(ref ssh_path) = project.ssh_key_path { mounts.push(Mount { target: Some("/home/claude/.ssh".to_string()), source: Some(ssh_path.clone()), typ: Some(MountTypeEnum::BIND), read_only: Some(true), ..Default::default() }); } // Docker socket (only if allowed) if project.allow_docker_access { mounts.push(Mount { target: Some("/var/run/docker.sock".to_string()), source: Some(docker_socket_path.to_string()), typ: Some(MountTypeEnum::BIND), read_only: Some(false), ..Default::default() }); } let mut labels = HashMap::new(); labels.insert("triple-c.managed".to_string(), "true".to_string()); labels.insert("triple-c.project-id".to_string(), project.id.clone()); labels.insert("triple-c.project-name".to_string(), project.name.clone()); let host_config = HostConfig { mounts: Some(mounts), ..Default::default() }; let config = Config { image: Some(image), hostname: Some("triple-c".to_string()), env: Some(env_vars), labels: Some(labels), working_dir: Some("/workspace".to_string()), host_config: Some(host_config), tty: Some(true), ..Default::default() }; let options = CreateContainerOptions { name: container_name, ..Default::default() }; let response = docker .create_container(Some(options), config) .await .map_err(|e| format!("Failed to create container: {}", e))?; Ok(response.id) } pub async fn start_container(container_id: &str) -> Result<(), String> { let docker = get_docker()?; docker .start_container(container_id, None::>) .await .map_err(|e| format!("Failed to start container: {}", e)) } pub async fn stop_container(container_id: &str) -> Result<(), String> { let docker = get_docker()?; docker .stop_container( container_id, Some(StopContainerOptions { t: 10 }), ) .await .map_err(|e| format!("Failed to stop container: {}", e)) } pub async fn remove_container(container_id: &str) -> Result<(), String> { let docker = get_docker()?; docker .remove_container( container_id, Some(RemoveContainerOptions { force: true, ..Default::default() }), ) .await .map_err(|e| format!("Failed to remove container: {}", e)) } pub async fn get_container_info(project: &Project) -> Result, String> { if let Some(ref container_id) = project.container_id { let docker = get_docker()?; match docker.inspect_container(container_id, None).await { Ok(info) => { let status = info .state .and_then(|s| s.status) .map(|s| format!("{:?}", s)) .unwrap_or_else(|| "unknown".to_string()); Ok(Some(ContainerInfo { container_id: container_id.clone(), project_id: project.id.clone(), status, image: container_config::full_image_name(), })) } Err(_) => Ok(None), } } else { Ok(None) } } pub async fn list_sibling_containers() -> Result, String> { let docker = get_docker()?; let all_containers: Vec = docker .list_containers(Some(ListContainersOptions:: { all: true, ..Default::default() })) .await .map_err(|e| format!("Failed to list containers: {}", e))?; // Filter out Triple-C managed containers let siblings: Vec = all_containers .into_iter() .filter(|c| { if let Some(labels) = &c.labels { !labels.contains_key("triple-c.managed") } else { true } }) .collect(); Ok(siblings) }