Skip to content

Commit 5e729c9

Browse files
author
EchoBT
committed
feat(orchestrator): add ContainerBackend abstraction for secure mode
Add backend abstraction that allows validators to use either: - Direct Docker (default, for local development) - Secure broker via Unix socket (for production) Set CONTAINER_BROKER_SOCKET env var to enable secure mode. Usage: let backend = create_backend().await?; backend.start_challenge(&config).await?; The backend is automatically selected based on environment.
1 parent 38862c4 commit 5e729c9

4 files changed

Lines changed: 288 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/challenge-orchestrator/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ description = "Docker-based challenge container orchestration for Mini-Chain"
88

99
[dependencies]
1010
platform-core = { path = "../core" }
11+
secure-container-runtime = { path = "../secure-container-runtime" }
1112

1213
# Async
1314
tokio = { workspace = true }
1415
futures = { workspace = true }
1516
async-trait = { workspace = true }
1617

17-
# Docker
18+
# Docker (for local development only)
1819
bollard = "0.18"
1920

2021
# Serialization
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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+
}

crates/challenge-orchestrator/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,25 @@
55
//! - Health monitoring
66
//! - Evaluation routing
77
//! - Hot-swap without core restart
8+
//!
9+
//! ## Backend Selection
10+
//!
11+
//! The orchestrator supports two backends:
12+
//! - **Direct Docker** (default): For local development and testing
13+
//! - **Secure Broker**: For production validators, uses Unix socket API
14+
//!
15+
//! Set `CONTAINER_BROKER_SOCKET=/var/run/platform/broker.sock` to use the secure broker.
816
17+
pub mod backend;
918
pub mod config;
1019
pub mod docker;
1120
pub mod evaluator;
1221
pub mod health;
1322
pub mod lifecycle;
1423

24+
pub use backend::{
25+
create_backend, is_secure_mode, ContainerBackend, DirectDockerBackend, SecureBackend,
26+
};
1527
pub use config::*;
1628
pub use docker::*;
1729
pub use evaluator::*;

0 commit comments

Comments
 (0)