Gunnar

mikronova/draupnir

tests/stage1_container_start_proof.rs

//! **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());
}