|
| 1 | +//! Container backend abstraction |
| 2 | +//! |
| 3 | +//! Provides a unified interface for container management that can use: |
| 4 | +//! - Direct Docker (for local development/testing) |
| 5 | +//! - SecureContainerClient via broker (for production validators) |
| 6 | +//! |
| 7 | +//! The backend is selected based on the CONTAINER_BROKER_SOCKET environment variable. |
| 8 | +//! If set, uses the secure broker. Otherwise, uses direct Docker. |
| 9 | +
|
| 10 | +use crate::{ChallengeContainerConfig, ChallengeInstance, ContainerStatus}; |
| 11 | +use async_trait::async_trait; |
| 12 | +use secure_container_runtime::{ |
| 13 | + ContainerConfigBuilder, ContainerState, NetworkMode, SecureContainerClient, |
| 14 | +}; |
| 15 | +use tracing::{info, warn}; |
| 16 | + |
| 17 | +/// Container backend trait for managing challenge containers |
| 18 | +#[async_trait] |
| 19 | +pub trait ContainerBackend: Send + Sync { |
| 20 | + /// Start a challenge container |
| 21 | + async fn start_challenge( |
| 22 | + &self, |
| 23 | + config: &ChallengeContainerConfig, |
| 24 | + ) -> anyhow::Result<ChallengeInstance>; |
| 25 | + |
| 26 | + /// Stop a container |
| 27 | + async fn stop_container(&self, container_id: &str) -> anyhow::Result<()>; |
| 28 | + |
| 29 | + /// Remove a container |
| 30 | + async fn remove_container(&self, container_id: &str) -> anyhow::Result<()>; |
| 31 | + |
| 32 | + /// Check if a container is running |
| 33 | + async fn is_container_running(&self, container_id: &str) -> anyhow::Result<bool>; |
| 34 | + |
| 35 | + /// Pull an image |
| 36 | + async fn pull_image(&self, image: &str) -> anyhow::Result<()>; |
| 37 | + |
| 38 | + /// Get container logs |
| 39 | + async fn get_logs(&self, container_id: &str, tail: usize) -> anyhow::Result<String>; |
| 40 | + |
| 41 | + /// Cleanup all containers for a challenge |
| 42 | + async fn cleanup_challenge(&self, challenge_id: &str) -> anyhow::Result<usize>; |
| 43 | + |
| 44 | + /// List containers for a challenge |
| 45 | + async fn list_challenge_containers(&self, challenge_id: &str) -> anyhow::Result<Vec<String>>; |
| 46 | +} |
| 47 | + |
| 48 | +/// Secure container backend using the broker |
| 49 | +pub struct SecureBackend { |
| 50 | + client: SecureContainerClient, |
| 51 | + validator_id: String, |
| 52 | +} |
| 53 | + |
| 54 | +impl SecureBackend { |
| 55 | + /// Create a new secure backend |
| 56 | + pub fn new(socket_path: &str, validator_id: &str) -> Self { |
| 57 | + Self { |
| 58 | + client: SecureContainerClient::new(socket_path), |
| 59 | + validator_id: validator_id.to_string(), |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + /// Create from environment |
| 64 | + pub fn from_env() -> Option<Self> { |
| 65 | + let socket = std::env::var("CONTAINER_BROKER_SOCKET").ok()?; |
| 66 | + let validator_id = |
| 67 | + std::env::var("VALIDATOR_HOTKEY").unwrap_or_else(|_| "unknown".to_string()); |
| 68 | + Some(Self::new(&socket, &validator_id)) |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +#[async_trait] |
| 73 | +impl ContainerBackend for SecureBackend { |
| 74 | + async fn start_challenge( |
| 75 | + &self, |
| 76 | + config: &ChallengeContainerConfig, |
| 77 | + ) -> anyhow::Result<ChallengeInstance> { |
| 78 | + info!( |
| 79 | + challenge = %config.name, |
| 80 | + image = %config.docker_image, |
| 81 | + "Starting challenge via secure broker" |
| 82 | + ); |
| 83 | + |
| 84 | + // Build container config |
| 85 | + let container_config = ContainerConfigBuilder::new( |
| 86 | + &config.docker_image, |
| 87 | + &config.challenge_id.to_string(), |
| 88 | + &self.validator_id, |
| 89 | + ) |
| 90 | + .memory((config.memory_mb * 1024 * 1024) as i64) |
| 91 | + .cpu(config.cpu_cores) |
| 92 | + .network_mode(NetworkMode::Isolated) |
| 93 | + .expose(8080) |
| 94 | + .env("CHALLENGE_ID", &config.challenge_id.to_string()) |
| 95 | + .env("MECHANISM_ID", &config.mechanism_id.to_string()) |
| 96 | + .build(); |
| 97 | + |
| 98 | + // Create and start container |
| 99 | + let (container_id, _container_name) = self |
| 100 | + .client |
| 101 | + .create_container(container_config) |
| 102 | + .await |
| 103 | + .map_err(|e| anyhow::anyhow!("Failed to create container: {}", e))?; |
| 104 | + |
| 105 | + self.client |
| 106 | + .start_container(&container_id) |
| 107 | + .await |
| 108 | + .map_err(|e| anyhow::anyhow!("Failed to start container: {}", e))?; |
| 109 | + |
| 110 | + // Get endpoint |
| 111 | + let endpoint = self |
| 112 | + .client |
| 113 | + .get_endpoint(&container_id, 8080) |
| 114 | + .await |
| 115 | + .map_err(|e| anyhow::anyhow!("Failed to get endpoint: {}", e))?; |
| 116 | + |
| 117 | + info!( |
| 118 | + container_id = %container_id, |
| 119 | + endpoint = %endpoint, |
| 120 | + "Challenge container started via broker" |
| 121 | + ); |
| 122 | + |
| 123 | + Ok(ChallengeInstance { |
| 124 | + challenge_id: config.challenge_id, |
| 125 | + container_id, |
| 126 | + image: config.docker_image.clone(), |
| 127 | + endpoint, |
| 128 | + started_at: chrono::Utc::now(), |
| 129 | + status: ContainerStatus::Running, |
| 130 | + }) |
| 131 | + } |
| 132 | + |
| 133 | + async fn stop_container(&self, container_id: &str) -> anyhow::Result<()> { |
| 134 | + self.client |
| 135 | + .stop_container(container_id, 30) |
| 136 | + .await |
| 137 | + .map_err(|e| anyhow::anyhow!("Failed to stop container: {}", e)) |
| 138 | + } |
| 139 | + |
| 140 | + async fn remove_container(&self, container_id: &str) -> anyhow::Result<()> { |
| 141 | + self.client |
| 142 | + .remove_container(container_id, true) |
| 143 | + .await |
| 144 | + .map_err(|e| anyhow::anyhow!("Failed to remove container: {}", e)) |
| 145 | + } |
| 146 | + |
| 147 | + async fn is_container_running(&self, container_id: &str) -> anyhow::Result<bool> { |
| 148 | + match self.client.inspect(container_id).await { |
| 149 | + Ok(info) => Ok(info.state == ContainerState::Running), |
| 150 | + Err(_) => Ok(false), |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + async fn pull_image(&self, image: &str) -> anyhow::Result<()> { |
| 155 | + self.client |
| 156 | + .pull_image(image) |
| 157 | + .await |
| 158 | + .map_err(|e| anyhow::anyhow!("Failed to pull image: {}", e)) |
| 159 | + } |
| 160 | + |
| 161 | + async fn get_logs(&self, container_id: &str, tail: usize) -> anyhow::Result<String> { |
| 162 | + self.client |
| 163 | + .logs(container_id, tail) |
| 164 | + .await |
| 165 | + .map_err(|e| anyhow::anyhow!("Failed to get logs: {}", e)) |
| 166 | + } |
| 167 | + |
| 168 | + async fn cleanup_challenge(&self, challenge_id: &str) -> anyhow::Result<usize> { |
| 169 | + let result = self |
| 170 | + .client |
| 171 | + .cleanup_challenge(challenge_id) |
| 172 | + .await |
| 173 | + .map_err(|e| anyhow::anyhow!("Failed to cleanup: {}", e))?; |
| 174 | + |
| 175 | + if !result.success() { |
| 176 | + warn!(errors = ?result.errors, "Some cleanup errors occurred"); |
| 177 | + } |
| 178 | + |
| 179 | + Ok(result.removed) |
| 180 | + } |
| 181 | + |
| 182 | + async fn list_challenge_containers(&self, challenge_id: &str) -> anyhow::Result<Vec<String>> { |
| 183 | + let containers = self |
| 184 | + .client |
| 185 | + .list_by_challenge(challenge_id) |
| 186 | + .await |
| 187 | + .map_err(|e| anyhow::anyhow!("Failed to list containers: {}", e))?; |
| 188 | + |
| 189 | + Ok(containers.into_iter().map(|c| c.id).collect()) |
| 190 | + } |
| 191 | +} |
| 192 | + |
| 193 | +/// Direct Docker backend (for local development) |
| 194 | +pub struct DirectDockerBackend { |
| 195 | + docker: crate::docker::DockerClient, |
| 196 | +} |
| 197 | + |
| 198 | +impl DirectDockerBackend { |
| 199 | + /// Create a new direct Docker backend |
| 200 | + pub async fn new() -> anyhow::Result<Self> { |
| 201 | + let docker = crate::docker::DockerClient::connect().await?; |
| 202 | + Ok(Self { docker }) |
| 203 | + } |
| 204 | +} |
| 205 | + |
| 206 | +#[async_trait] |
| 207 | +impl ContainerBackend for DirectDockerBackend { |
| 208 | + async fn start_challenge( |
| 209 | + &self, |
| 210 | + config: &ChallengeContainerConfig, |
| 211 | + ) -> anyhow::Result<ChallengeInstance> { |
| 212 | + self.docker.start_challenge(config).await |
| 213 | + } |
| 214 | + |
| 215 | + async fn stop_container(&self, container_id: &str) -> anyhow::Result<()> { |
| 216 | + self.docker.stop_container(container_id).await |
| 217 | + } |
| 218 | + |
| 219 | + async fn remove_container(&self, container_id: &str) -> anyhow::Result<()> { |
| 220 | + self.docker.remove_container(container_id).await |
| 221 | + } |
| 222 | + |
| 223 | + async fn is_container_running(&self, container_id: &str) -> anyhow::Result<bool> { |
| 224 | + self.docker.is_container_running(container_id).await |
| 225 | + } |
| 226 | + |
| 227 | + async fn pull_image(&self, image: &str) -> anyhow::Result<()> { |
| 228 | + self.docker.pull_image(image).await |
| 229 | + } |
| 230 | + |
| 231 | + async fn get_logs(&self, container_id: &str, tail: usize) -> anyhow::Result<String> { |
| 232 | + self.docker.get_logs(container_id, tail).await |
| 233 | + } |
| 234 | + |
| 235 | + async fn cleanup_challenge(&self, challenge_id: &str) -> anyhow::Result<usize> { |
| 236 | + let containers = self.docker.list_challenge_containers().await?; |
| 237 | + let mut removed = 0; |
| 238 | + |
| 239 | + for container_id in containers { |
| 240 | + if container_id.contains(&challenge_id.to_string()) { |
| 241 | + let _ = self.docker.stop_container(&container_id).await; |
| 242 | + if self.docker.remove_container(&container_id).await.is_ok() { |
| 243 | + removed += 1; |
| 244 | + } |
| 245 | + } |
| 246 | + } |
| 247 | + |
| 248 | + Ok(removed) |
| 249 | + } |
| 250 | + |
| 251 | + async fn list_challenge_containers(&self, _challenge_id: &str) -> anyhow::Result<Vec<String>> { |
| 252 | + self.docker.list_challenge_containers().await |
| 253 | + } |
| 254 | +} |
| 255 | + |
| 256 | +/// Create the appropriate backend based on environment |
| 257 | +pub async fn create_backend() -> anyhow::Result<Box<dyn ContainerBackend>> { |
| 258 | + // Check if broker socket is configured |
| 259 | + if let Some(secure) = SecureBackend::from_env() { |
| 260 | + info!("Using secure container broker"); |
| 261 | + return Ok(Box::new(secure)); |
| 262 | + } |
| 263 | + |
| 264 | + // Fall back to direct Docker |
| 265 | + info!("Using direct Docker (local development mode)"); |
| 266 | + let direct = DirectDockerBackend::new().await?; |
| 267 | + Ok(Box::new(direct)) |
| 268 | +} |
| 269 | + |
| 270 | +/// Check if running in secure mode (broker available) |
| 271 | +pub fn is_secure_mode() -> bool { |
| 272 | + std::env::var("CONTAINER_BROKER_SOCKET").is_ok() |
| 273 | +} |
0 commit comments