1use crate::backend::volume::{VolumeMount, VolumeValidator};
7use crate::backend::{Backend, Cmd, RunResult};
8use crate::error::{BackendError, Result};
9use crate::policy::Policy;
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12use testcontainers::{core::ExecCommand, runners::SyncRunner, GenericImage, ImageExt};
13
14use tracing::{info, instrument, warn};
15
16#[derive(Debug, Clone)]
18pub struct TestcontainerBackend {
19 image_name: String,
21 image_tag: String,
22 policy: Policy,
24 timeout: Duration,
26 startup_timeout: Duration,
28 env_vars: std::collections::HashMap<String, String>,
30 default_command: Option<Vec<String>>,
32 volume_mounts: Vec<VolumeMount>,
34 volume_validator: Arc<VolumeValidator>,
36 memory_limit: Option<u64>,
38 cpu_limit: Option<f64>,
40 determinism_engine: Option<Arc<crate::determinism::DeterminismEngine>>,
42}
43
44impl TestcontainerBackend {
45 pub fn new(image: impl Into<String>) -> Result<Self> {
47 let image_str = image.into();
48
49 let (image_name, image_tag) = if let Some((name, tag)) = image_str.split_once(':') {
51 (name.to_string(), tag.to_string())
52 } else {
53 (image_str, "latest".to_string())
54 };
55
56 Ok(Self {
57 image_name,
58 image_tag,
59 policy: Policy::default(),
60 timeout: Duration::from_secs(30), startup_timeout: Duration::from_secs(10), env_vars: std::collections::HashMap::new(),
63 default_command: None,
64 volume_mounts: Vec::new(),
65 volume_validator: Arc::new(VolumeValidator::default()),
66 memory_limit: None,
67 cpu_limit: None,
68 determinism_engine: None,
69 })
70 }
71
72 pub fn with_policy(mut self, policy: Policy) -> Self {
74 self.policy = policy;
75 self
76 }
77
78 pub fn with_timeout(mut self, timeout: Duration) -> Self {
80 self.timeout = timeout;
81 self
82 }
83
84 pub fn with_startup_timeout(mut self, timeout: Duration) -> Self {
86 self.startup_timeout = timeout;
87 self
88 }
89
90 pub fn is_running(&self) -> bool {
92 true
95 }
96
97 pub fn with_env(mut self, key: &str, val: &str) -> Self {
99 self.env_vars.insert(key.to_string(), val.to_string());
100 self
101 }
102
103 pub fn with_cmd(mut self, cmd: Vec<String>) -> Self {
105 self.default_command = Some(cmd);
106 self
107 }
108
109 pub fn with_volume(
121 mut self,
122 host_path: &str,
123 container_path: &str,
124 read_only: bool,
125 ) -> Result<Self> {
126 let mount = VolumeMount::new(host_path, container_path, read_only)?;
127 self.volume_validator.validate(&mount)?;
128 self.volume_mounts.push(mount);
129 Ok(self)
130 }
131
132 pub fn with_volume_ro(self, host_path: &str, container_path: &str) -> Result<Self> {
136 self.with_volume(host_path, container_path, true)
137 }
138
139 pub fn with_volume_validator(mut self, validator: VolumeValidator) -> Self {
141 self.volume_validator = Arc::new(validator);
142 self
143 }
144
145 pub fn volumes(&self) -> &[VolumeMount] {
147 &self.volume_mounts
148 }
149
150 pub fn with_memory_limit(mut self, limit_mb: u64) -> Self {
152 self.memory_limit = Some(limit_mb);
153 self
154 }
155
156 pub fn with_cpu_limit(mut self, cpus: f64) -> Self {
158 self.cpu_limit = Some(cpus);
159 self
160 }
161
162 pub fn with_determinism(mut self, engine: Arc<crate::determinism::DeterminismEngine>) -> Self {
167 self.determinism_engine = Some(engine);
168 self
169 }
170
171 pub fn is_available() -> bool {
173 true
175 }
176
177 pub fn validate_otel_instrumentation(&self) -> Result<bool> {
185 use crate::telemetry::validation::is_otel_initialized;
187
188 if !is_otel_initialized() {
189 return Err(crate::error::CleanroomError::validation_error(
190 "OpenTelemetry is not initialized. Enable OTEL features and call init_otel()",
191 ));
192 }
193
194 Ok(true)
197 }
198
199 pub fn otel_validation_enabled(&self) -> bool {
201 true
202 }
203
204 #[instrument(name = "clnrm.container.exec", skip(self, cmd), fields(container.image = %self.image_name, container.tag = %self.image_tag, component = "container_backend"))]
206 fn execute_in_container(&self, cmd: &Cmd) -> Result<RunResult> {
207 let start_time = Instant::now();
208
209 info!(
210 "Starting container with image {}:{}",
211 self.image_name, self.image_tag
212 );
213
214 #[allow(unused_variables)]
216 let container_id = uuid::Uuid::new_v4().to_string();
217
218 {
219 use crate::telemetry::events;
220 use opentelemetry::global;
221 use opentelemetry::trace::{Span, Tracer, TracerProvider};
222
223 let tracer_provider = global::tracer_provider();
225 let mut span = tracer_provider
226 .tracer("clnrm-backend")
227 .start("clnrm.container.start");
228
229 events::record_container_start(
230 &mut span,
231 &format!("{}:{}", self.image_name, self.image_tag),
232 &container_id,
233 );
234 span.end();
235 }
236
237 let image = GenericImage::new(self.image_name.clone(), self.image_tag.clone());
241
242 let mut container_request: testcontainers::core::ContainerRequest<
244 testcontainers::GenericImage,
245 > = image.into();
246
247 for (key, value) in &self.env_vars {
249 container_request = container_request.with_env_var(key, value);
250 }
251
252 for (key, value) in &cmd.env {
254 container_request = container_request.with_env_var(key, value);
255 }
256
257 for (key, value) in self.policy.to_env() {
259 container_request = container_request.with_env_var(key, value);
260 }
261
262 if let Some(ref engine) = self.determinism_engine {
264 if engine.get_seed().is_some() {
266 let random_value = match engine.next_u32() {
268 Ok(val) => val,
269 Err(e) => {
270 warn!("Failed to generate random value from seed: {}", e);
271 0
272 }
273 };
274 container_request =
275 container_request.with_env_var("RANDOM", random_value.to_string());
276 }
277
278 if let Some(frozen_clock) = engine.get_frozen_clock() {
280 container_request = container_request.with_env_var("FAKETIME", frozen_clock);
281 container_request = container_request.with_env_var(
284 "LD_PRELOAD",
285 "/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1",
286 );
287 container_request = container_request.with_env_var("FAKETIME_NO_CACHE", "1");
289 }
290
291 if engine.config().has_deterministic_ports() {
293 if let Ok(port_list) = engine.get_port_pool_env() {
294 container_request =
295 container_request.with_env_var("CLEANROOM_ALLOWED_PORTS", port_list);
296 }
297 }
298 }
299
300 for mount in &self.volume_mounts {
302 use testcontainers::core::{AccessMode, Mount};
303
304 let access_mode = if mount.is_read_only() {
305 AccessMode::ReadOnly
306 } else {
307 AccessMode::ReadWrite
308 };
309
310 let bind_mount = Mount::bind_mount(
311 mount.host_path().to_string_lossy().to_string(),
312 mount.container_path().to_string_lossy().to_string(),
313 )
314 .with_access_mode(access_mode);
315
316 container_request = container_request.with_mount(bind_mount);
317 }
318
319 container_request = container_request.with_cmd(vec!["sleep", "3600"]);
322
323 if let Some(workdir) = &cmd.workdir {
325 container_request =
326 container_request.with_working_dir(workdir.to_string_lossy().to_string());
327 }
328
329 let container_start_time = Instant::now();
331 let container = container_request
332 .start()
333 .map_err(|e| {
334 let elapsed = container_start_time.elapsed();
335 if elapsed > Duration::from_secs(10) {
336 warn!("Container startup took {}s, which is longer than expected. First pull of image may take time.", elapsed.as_secs());
337 }
338
339 BackendError::Runtime(format!(
340 "Failed to start container with image '{}:{}' after {}s.\n\
341 Possible causes:\n\
342 - Docker daemon not running (try: docker ps)\n\
343 - Image needs to be pulled (first run may take longer)\n\
344 - Network issues preventing image pull\n\
345 Try: Increase startup timeout or check Docker status\n\
346 Original error: {}",
347 self.image_name, self.image_tag, elapsed.as_secs(), e
348 ))
349 })?;
350
351 info!("Container started successfully, executing command");
352
353 let cmd_args: Vec<&str> = std::iter::once(cmd.bin.as_str())
355 .chain(cmd.args.iter().map(|s| s.as_str()))
356 .collect();
357
358 #[allow(unused_variables)]
359 let cmd_string = format!("{} {}", cmd.bin, cmd.args.join(" "));
360
361 let exec_cmd = ExecCommand::new(cmd_args);
362 let mut exec_result = container
363 .exec(exec_cmd)
364 .map_err(|e| BackendError::Runtime(format!("Command execution failed: {}", e)))?;
365
366 let duration_ms = start_time.elapsed().as_millis() as u64;
367
368 info!("Command completed in {}ms", duration_ms);
369
370 use std::io::Read;
372 let mut stdout = String::new();
373 let mut stderr = String::new();
374
375 exec_result
376 .stdout()
377 .read_to_string(&mut stdout)
378 .map_err(|e| BackendError::Runtime(format!("Failed to read stdout: {}", e)))?;
379 exec_result
380 .stderr()
381 .read_to_string(&mut stderr)
382 .map_err(|e| BackendError::Runtime(format!("Failed to read stderr: {}", e)))?;
383
384 #[allow(clippy::unnecessary_lazy_evaluations)] let exit_code = exec_result
388 .exit_code()
389 .map_err(|e| BackendError::Runtime(format!("Failed to get exit code: {}", e)))?
390 .unwrap_or_else(|| {
391 warn!("Exit code unavailable from container, defaulting to -1");
394 -1
395 }) as i32;
396
397 {
398 use crate::telemetry::events;
399 use opentelemetry::global;
400 use opentelemetry::trace::{Span, Tracer, TracerProvider};
401
402 let tracer_provider = global::tracer_provider();
404 let mut exec_span = tracer_provider
405 .tracer("clnrm-backend")
406 .start("clnrm.container.exec");
407
408 events::record_container_exec(&mut exec_span, &cmd_string, exit_code);
409 exec_span.end();
410
411 let mut stop_span = tracer_provider
413 .tracer("clnrm-backend")
414 .start("clnrm.container.stop");
415
416 events::record_container_stop(&mut stop_span, &container_id, exit_code);
417 stop_span.end();
418 }
419
420 Ok(RunResult {
421 exit_code,
422 stdout,
423 stderr,
424 duration_ms,
425 steps: Vec::new(),
426 redacted_env: Vec::new(),
427 backend: "testcontainers".to_string(),
428 concurrent: false,
429 step_order: Vec::new(),
430 })
431 }
432}
433
434impl Backend for TestcontainerBackend {
435 fn run_cmd(&self, cmd: Cmd) -> Result<RunResult> {
436 let start_time = Instant::now();
438
439 let result = self.execute_in_container(&cmd)?;
441
442 if start_time.elapsed() > self.timeout {
444 return Err(crate::error::CleanroomError::timeout_error(format!(
445 "Command execution timed out after {} seconds",
446 self.timeout.as_secs()
447 )));
448 }
449
450 Ok(result)
451 }
452
453 fn name(&self) -> &str {
454 "testcontainers"
455 }
456
457 fn is_available(&self) -> bool {
458 Self::is_available()
459 }
460
461 fn supports_hermetic(&self) -> bool {
462 true
463 }
464
465 fn supports_deterministic(&self) -> bool {
466 true
467 }
468}