#![cfg(all(unix, feature = "docker-integration"))]
use std::net::Ipv4Addr;
use std::process::Stdio;
use kamaji::docker::DockerRuntime;
use kamaji::{Kamaji, MeshAssignment, WorkloadStatus};
use workload_spec::{ImageRef, MeshIdent, ResourceLimits, TierTag, WorkloadSpec};
async fn docker_available() -> bool {
let out = tokio::process::Command::new("docker")
.args(["version"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await;
matches!(out, Ok(s) if s.success())
}
fn sleeper_spec(id: &str) -> WorkloadSpec {
let mut spec = WorkloadSpec::for_forge(
id,
ImageRef {
registry: "docker.io".into(),
repository: "library/alpine".into(),
tag: "3.20".into(),
digest: ImageRef::UNPINNED_DIGEST.into(),
},
TierTag("private".into()),
vec![],
);
spec.command = Some(vec!["sleep".into(), "300".into()]);
spec.resources = ResourceLimits {
memory_mb: 64,
cpu_millis: 250,
memory_request_mb: None,
cpu_limit_millis: None,
pids_max: None,
scratch_floor_mb: None,
};
spec
}
#[tokio::test]
async fn deploy_list_stop_round_trips_against_live_docker() {
if !docker_available().await {
eprintln!("SKIP: docker not reachable");
return;
}
let rt = DockerRuntime::new();
let forge_id = "r626f1-live";
let spec = sleeper_spec(forge_id);
let ident = spec.expose.mesh.identity.clone();
let mesh = MeshAssignment::inlined(Ipv4Addr::new(127, 0, 0, 1));
rt.teardown_workload(&ident).await.unwrap();
let deployed = rt
.deploy_workload(&spec, &mesh)
.await
.expect("deploy against live docker");
assert!(
!deployed.container_id.is_empty(),
"expected a container id from docker run"
);
assert!(
deployed.task_pid > 0,
"expected a live host pid from .State.Pid, got {}",
deployed.task_pid
);
let got = rt
.get_workload(&ident)
.await
.unwrap()
.expect("deployed workload is inspectable");
assert_eq!(got.status, WorkloadStatus::Running, "state: {got:?}");
assert_eq!(got.container_id, deployed.container_id);
let listed = rt.list_workloads_detailed().await.unwrap();
let mine: Vec<_> = listed.iter().filter(|w| w.state.ident == ident).collect();
assert_eq!(
mine.len(),
1,
"expected exactly one row for {ident:?}, got {mine:#?}"
);
assert_eq!(mine[0].pid, Some(deployed.task_pid));
assert_eq!(mine[0].state.status, WorkloadStatus::Running);
rt.teardown_workload(&ident).await.unwrap();
assert!(
rt.get_workload(&ident).await.unwrap().is_none(),
"workload still present after teardown"
);
rt.teardown_workload(&ident).await.unwrap();
let after = rt.list_workloads_detailed().await.unwrap();
assert!(
!after.iter().any(|w| w.state.ident == ident),
"torn-down workload still listed"
);
}
#[tokio::test]
async fn exited_container_reports_failed_with_no_pid() {
if !docker_available().await {
eprintln!("SKIP: docker not reachable");
return;
}
let rt = DockerRuntime::new();
let mut spec = sleeper_spec("r626f1-exit");
spec.command = Some(vec!["sh".into(), "-c".into(), "exit 3".into()]);
let ident = spec.expose.mesh.identity.clone();
let mesh = MeshAssignment::inlined(Ipv4Addr::new(127, 0, 0, 1));
rt.teardown_workload(&ident).await.unwrap();
let _ = rt.deploy_workload(&spec, &mesh).await;
for _ in 0..40 {
let state = rt.get_workload(&ident).await.unwrap();
if matches!(
state.as_ref().map(|s| &s.status),
Some(WorkloadStatus::Failed { .. })
) {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
let got = rt.get_workload(&ident).await.unwrap().expect("inspectable");
match &got.status {
WorkloadStatus::Failed { reason, .. } => {
assert!(reason.contains('3'), "expected exit code 3 in {reason:?}");
}
other => panic!("expected Failed, got {other:?}"),
}
let listed = rt.list_workloads_detailed().await.unwrap();
let mine = listed
.iter()
.find(|w| w.state.ident == ident)
.expect("exited container still listed (docker ps -a)");
assert_eq!(mine.pid, None, "exited container must not report a pid");
rt.teardown_workload(&ident).await.unwrap();
}
#[tokio::test]
async fn teardown_of_unknown_identity_is_a_noop() {
if !docker_available().await {
eprintln!("SKIP: docker not reachable");
return;
}
let rt = DockerRuntime::new();
rt.teardown_workload(&MeshIdent("r626f1-never-existed".into()))
.await
.unwrap();
rt.teardown_by_key("r626f1-never-existed").await.unwrap();
}
#[tokio::test]
async fn resolve_and_teardown_accept_either_id_or_mesh_identity() {
if !docker_available().await {
eprintln!("SKIP: docker not reachable");
return;
}
let rt = DockerRuntime::new();
let spec = sleeper_spec("r626f1-keys");
let ident = spec.expose.mesh.identity.clone();
let workload_id = spec.name.clone();
assert_ne!(
workload_id, ident.0,
"fixture must exercise the id/identity divergence"
);
rt.teardown_by_key(&workload_id).await.unwrap();
let mesh = MeshAssignment::inlined(Ipv4Addr::new(127, 0, 0, 1));
rt.deploy_workload(&spec, &mesh).await.expect("deploy");
let by_ident = rt.resolve(&ident.0).await.unwrap().expect("found by ident");
let by_id = rt
.resolve(&workload_id)
.await
.unwrap()
.expect("found by workload id");
assert_eq!(by_ident.state.container_id, by_id.state.container_id);
assert_eq!(by_id.workload_id, workload_id);
assert_eq!(by_id.state.ident, ident);
rt.teardown_by_key(&workload_id).await.unwrap();
assert!(
rt.get_workload(&ident).await.unwrap().is_none(),
"teardown by workload id must remove the container"
);
}
#[tokio::test]
async fn unlabelled_containers_are_not_listed_as_workloads() {
if !docker_available().await {
eprintln!("SKIP: docker not reachable");
return;
}
let name = "r626f1-foreign-tenant";
let _ = tokio::process::Command::new("docker")
.args(["rm", "-f", name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await;
let run = tokio::process::Command::new("docker")
.args([
"run",
"-d",
"--name",
name,
"docker.io/library/alpine:3.20",
"sleep",
"60",
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await
.expect("spawn docker run");
assert!(run.success(), "failed to start the foreign container");
let listed = DockerRuntime::new()
.list_workloads_detailed()
.await
.unwrap();
assert!(
!listed.iter().any(|w| w.state.ident.0 == name),
"an unlabelled container must not be listed as a yah workload"
);
let _ = tokio::process::Command::new("docker")
.args(["rm", "-f", name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await;
}