//! **STAGE 1 — the honest gap-closer: a REAL container started from pure-Rust,
//! read back to readiness.** The container equivalent of the korp-kvm `BOOT-OK`
//! e2e. It boots a real OCI image through draupnir's [`ContainerBoot`] — the
//! Rust `bollard` → rootless-podman-socket path, **no** `podman`/`docker` CLI
//! shell-out anywhere on the boot path — asserts the container is RUNNING, then
//! reads a genuine **application readiness signal** back (a redis/valkey `PING`
//! answered `+PONG` over the published TCP port), and tears the container down.
//!
//! This is deliberately stronger than a bare TCP-open probe: `+PONG` proves the
//! service *inside* the container came up and is answering the redis protocol —
//! the container `BOOT-OK`.
//!
//! `#[ignore]` by default: it needs the `backend-oci` feature AND a live rootless
//! podman socket AND a local redis/valkey image, so a plain `cargo test` (CI, no
//! daemon) skips it. Run it on the box with podman:
//!
//! ```text
//! cargo test --features backend-oci --test stage1_container_start_proof -- --ignored --nocapture
//! ```
#![cfg(feature = "backend-oci")]
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::{Duration, Instant};
use draupnir::container::{ContainerBoot, ContainerControl, ContainerState};
use draupnir::{Boot, BootSpec};
/// The local redis/valkey-protocol image the box already has (built by the korp
/// infra pipeline). Overridable so the proof can point at any redis-family image.
fn proof_image() -> String {
std::env::var("STAGE1_IMAGE")
.unwrap_or_else(|_| "localhost/nordisk-falkordb-valkey:9".to_string())
}
/// Publish the container's 6379 on this HOST port (offset off 6379 to dodge a
/// stray redis already bound to the default port).
///
/// Overridable via `STAGE1_HOST_PORT`, for the same reason `STAGE1_IMAGE` is:
/// the offset is a *guess*, not a reservation. On 2026-08-01 the first real run
/// of this proof — the first ever, the file having been dark behind
/// `#![cfg(feature = "backend-oci")]` since it was written — died on
/// `rootlessport listen tcp 0.0.0.0:6399: bind: address already in use`, because
/// a concurrently-running `knut-robotui-falkor` container had published 6399.
/// A hard-coded port makes the proof fail for a reason that has nothing to do
/// with the boot path it exists to prove.
const DEFAULT_HOST_PORT: u16 = 6399;
/// The host port to publish on (`$STAGE1_HOST_PORT`, else [`DEFAULT_HOST_PORT`]).
fn host_port() -> u16 {
std::env::var("STAGE1_HOST_PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_HOST_PORT)
}
/// **Read the application readiness signal back**: connect to `port`, send an
/// inline redis `PING`, and return `true` once the server answers `+PONG`. Retries
/// until `timeout` so it rides out the container's start latency. This is the
/// container `BOOT-OK` — proof the *app* inside is live, not merely the port bound.
fn redis_pong(port: u16, timeout: Duration) -> bool {
let start = Instant::now();
let addr = format!("127.0.0.1:{port}");
while start.elapsed() < timeout {
if let Ok(mut sock) =
TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_millis(300))
{
let _ = sock.set_read_timeout(Some(Duration::from_millis(500)));
if sock.write_all(b"PING\r\n").is_ok() {
let mut buf = [0u8; 64];
if let Ok(n) = sock.read(&mut buf) {
if n > 0
&& buf[..n]
.windows(5)
.any(|w| w.eq_ignore_ascii_case(b"+pong"))
{
return true;
}
}
}
}
std::thread::sleep(Duration::from_millis(300));
}
false
}
#[test]
#[ignore = "needs a live rootless podman socket + a local redis/valkey image"]
fn container_starts_from_rust_and_answers_ping() {
let image = proof_image();
let host_port = host_port();
// Boot a real container: publish in-container 6379 on host `host_port`, all cores.
let cores = std::thread::available_parallelism()
.map(|n| n.get() as f64)
.unwrap_or(4.0);
let spec = BootSpec::container("stage1-proof", &image)
.with_port_map(host_port, 6379)
.with_cpus(cores);
spec.validate().expect("stage-1 proof spec is valid");
let boot = ContainerBoot::new();
let machine = boot.boot(&spec).expect(
"start a real container through draupnir's Rust OCI engine (rootless podman socket)",
);
// 1) The container process is RUNNING.
assert_eq!(
boot.container_state(&machine),
ContainerState::Running,
"container reports RUNNING right after boot"
);
// 2) Read the readiness signal back: the app answers redis PING with +PONG.
let ready = redis_pong(host_port, Duration::from_secs(30));
// Capture proof detail before teardown.
let state = boot.container_state(&machine);
let logs = boot.drain_logs(&machine);
// Always tear the container down (idempotent remove) — the container BOOT-OK
// must leave nothing running behind it.
boot.stop(&machine);
let after_stop = boot.container_state(&machine);
assert!(
ready,
"container app answered redis PING with +PONG on host :{host_port} (container BOOT-OK)"
);
assert_eq!(
after_stop,
ContainerState::Gone,
"container torn down after the proof"
);
println!("STAGE-1 CONTAINER BOOT-OK");
println!(" image : {image}");
println!(" container id : {}", machine.id);
println!(" running : {state:?} -> readiness +PONG on host :{host_port}");
println!(" after stop : {after_stop:?}");
if let Some(first) = logs.first() {
println!(" first log : {first}");
}
println!(" log lines : {}", logs.len());
}