#![cfg(feature = "docker-integration")]
use std::collections::HashMap;
use std::process::Stdio;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use serde::Deserialize;
use tokio::process::Command;
use workload_spec::{MeshIdent, WorkloadSpec};
use crate::{
Backend, DeployResult, Kamaji, LogEvent, LogOpts, LogStream, LogStreamKind, MeshAssignment,
RuntimeHealth, WorkloadState, WorkloadStatus,
};
#[derive(Debug, Deserialize)]
struct DockerInspect {
#[serde(rename = "Id")]
id: String,
#[serde(rename = "State")]
state: DockerState,
#[serde(rename = "RestartCount")]
restart_count: u32,
#[serde(rename = "Config")]
config: DockerConfig,
}
#[derive(Debug, Deserialize)]
struct DockerState {
#[serde(rename = "Status")]
status: String,
#[serde(rename = "Restarting")]
restarting: bool,
#[serde(rename = "ExitCode")]
exit_code: i32,
#[serde(rename = "FinishedAt")]
finished_at: String,
#[serde(rename = "Pid")]
pid: i32,
}
impl DockerState {
fn host_pid(&self) -> Option<u32> {
u32::try_from(self.pid).ok().filter(|p| *p != 0)
}
}
#[derive(Debug, Deserialize)]
struct DockerConfig {
#[serde(rename = "Labels")]
labels: Option<HashMap<String, String>>,
}
pub const WORKLOAD_ID_LABEL: &str = "yah.workload_id";
pub const PUBLISH_ANNOTATION: &str = "yah.docker.publish";
pub const NETWORK_ANNOTATION: &str = "yah.docker.network";
pub const NETWORK_ALIAS_ANNOTATION: &str = "yah.docker.network_alias";
#[derive(Debug, Clone)]
pub struct DockerWorkload {
pub state: WorkloadState,
pub pid: Option<u32>,
pub workload_id: String,
}
#[derive(Clone)]
pub struct DockerRuntime {
docker_host: String,
}
impl DockerRuntime {
pub fn new() -> Self {
Self {
docker_host: String::new(),
}
}
pub fn with_host(docker_host: impl Into<String>) -> Self {
Self {
docker_host: docker_host.into(),
}
}
fn cmd(&self) -> Command {
let mut cmd = Command::new("docker");
if !self.docker_host.is_empty() {
cmd.env("DOCKER_HOST", &self.docker_host);
}
cmd.kill_on_drop(true);
cmd
}
fn image_ref(spec: &WorkloadSpec) -> String {
spec.image.pull_ref()
}
fn container_name(ident: &MeshIdent) -> &str {
&ident.0
}
async fn inspect(&self, name: &str) -> Result<Option<DockerInspect>> {
let out = self
.cmd()
.args(["inspect", name])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await
.with_context(|| format!("spawning docker inspect {name}"))?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
if is_missing_container(&stderr) {
return Ok(None);
}
return Err(anyhow!("docker inspect {name} failed: {}", stderr.trim()));
}
let json = String::from_utf8_lossy(&out.stdout);
let list: Vec<DockerInspect> = serde_json::from_str(&json)
.with_context(|| format!("parsing docker inspect JSON for {name}"))?;
Ok(list.into_iter().next())
}
fn inspect_to_state(di: DockerInspect, ident: MeshIdent) -> WorkloadState {
Self::inspect_to_workload(di, ident).state
}
fn inspect_to_workload(di: DockerInspect, ident: MeshIdent) -> DockerWorkload {
let pid = di.state.host_pid();
let labels = di.config.labels.unwrap_or_default();
let mesh_ip = labels.get("yah.mesh_ip").and_then(|s| s.parse().ok());
let workload_id = labels
.get(WORKLOAD_ID_LABEL)
.cloned()
.unwrap_or_else(|| ident.0.clone());
let status = map_docker_state(&di.state, di.restart_count);
DockerWorkload {
state: WorkloadState {
ident,
container_id: di.id,
status,
mesh_ip,
},
pid,
workload_id,
}
}
pub async fn resolve(&self, key: &str) -> Result<Option<DockerWorkload>> {
if let Some(di) = self.inspect(key).await? {
let ident = Self::ident_of(&di, key);
return Ok(Some(Self::inspect_to_workload(di, ident)));
}
Ok(self
.list_workloads_detailed()
.await?
.into_iter()
.find(|w| w.workload_id == key))
}
pub async fn teardown_by_key(&self, key: &str) -> Result<()> {
let ident = match self.resolve(key).await? {
Some(w) => w.state.ident,
None => return Ok(()),
};
self.teardown_workload(&ident).await
}
pub async fn list_workloads_detailed(&self) -> Result<Vec<DockerWorkload>> {
let mut out = Vec::new();
for name in self.yah_container_names().await? {
if let Some(di) = self.inspect(&name).await? {
let ident = Self::ident_of(&di, &name);
out.push(Self::inspect_to_workload(di, ident));
}
}
Ok(out)
}
async fn yah_container_names(&self) -> Result<Vec<String>> {
let out = self
.cmd()
.args([
"ps",
"-a",
"--filter",
"label=yah.ident",
"--format",
"{{.Names}}",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await
.context("docker ps for yah workloads")?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(anyhow!("docker ps failed: {}", stderr.trim()));
}
let stdout = String::from_utf8_lossy(&out.stdout);
Ok(stdout
.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.map(str::to_string)
.collect())
}
fn run_args(
spec: &WorkloadSpec,
mesh: &MeshAssignment,
image: &str,
name: &str,
) -> Result<Vec<String>> {
let ident = &spec.expose.mesh.identity;
let mut args: Vec<String> = vec![
"run".into(),
"-d".into(),
"--name".into(),
name.to_string(),
"--label".into(),
format!("yah.ident={}", ident.0),
"--label".into(),
format!("{WORKLOAD_ID_LABEL}={}", spec.name),
"--label".into(),
format!("yah.mesh_ip={}", mesh.mesh_ip),
];
let mut labels: Vec<(&String, &String)> = spec.labels.iter().collect();
labels.sort();
for (k, v) in labels {
args.push("--label".into());
args.push(format!("{k}={v}"));
}
if spec.resources.memory_mb > 0 {
args.push("--memory".into());
args.push(format!("{}m", spec.resources.memory_mb));
}
if spec.resources.cpu_millis > 0 {
args.push("--cpu-shares".into());
args.push(spec.resources.cpu_shares().to_string());
}
args.push("--restart".into());
args.push(restart_flag(&spec.restart_policy));
for (host, container) in parse_publish(spec)? {
args.push("-p".into());
args.push(format!("{host}:{container}"));
}
if let Some(net) = docker_network(spec) {
args.push("--network".into());
args.push(net.to_string());
for alias in network_aliases(spec) {
args.push("--network-alias".into());
args.push(alias.to_string());
}
}
for v in &spec.volumes {
let target = v.target.to_string_lossy();
match &v.source {
workload_spec::VolumeSource::Bind { host_path } => {
args.push("-v".into());
args.push(format!(
"{}:{target}{}",
host_path.to_string_lossy(),
if v.read_only { ":ro" } else { "" }
));
}
workload_spec::VolumeSource::Named { name } => {
args.push("-v".into());
args.push(format!(
"{name}:{target}{}",
if v.read_only { ":ro" } else { "" }
));
}
workload_spec::VolumeSource::Tmpfs { size_mb } => {
args.push("--tmpfs".into());
args.push(format!("{target}:size={size_mb}m"));
}
}
}
args.push("--env".into());
args.push(format!("YAH_MESH_IP={}", mesh.mesh_ip));
for e in &spec.env {
if let workload_spec::EnvValue::Literal { value } = &e.value {
args.push("--env".into());
args.push(format!("{}={}", e.name, value));
}
}
args.push(image.to_string());
if let Some(cmd) = &spec.command {
args.extend(cmd.iter().cloned());
}
Ok(args)
}
pub async fn ensure_network(&self, name: &str) -> Result<bool> {
let exists = self
.cmd()
.args(["network", "inspect", name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await
.with_context(|| format!("spawning docker network inspect {name}"))?;
if exists.success() {
return Ok(false);
}
let create = self
.cmd()
.args(["network", "create", name])
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()
.await
.with_context(|| format!("spawning docker network create {name}"))?;
if create.status.success() {
return Ok(true);
}
let stderr = String::from_utf8_lossy(&create.stderr);
if stderr.to_ascii_lowercase().contains("already exists") {
return Ok(false);
}
Err(anyhow!(
"docker network create {name} failed: {}",
stderr.trim()
))
}
fn ident_of(di: &DockerInspect, name: &str) -> MeshIdent {
MeshIdent(
di.config
.labels
.as_ref()
.and_then(|l| l.get("yah.ident"))
.cloned()
.unwrap_or_else(|| name.to_string()),
)
}
}
impl Default for DockerRuntime {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Kamaji for DockerRuntime {
fn backend(&self) -> Backend {
Backend::Docker
}
async fn deploy_workload(
&self,
spec: &WorkloadSpec,
mesh: &MeshAssignment,
) -> Result<DeployResult> {
let ident = &spec.expose.mesh.identity;
let name = Self::container_name(ident).to_string();
let image = Self::image_ref(spec);
let _ = self.teardown_workload(ident).await;
let pull = self
.cmd()
.args(["pull", &image])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await
.with_context(|| format!("docker pull {image}"))?;
if !pull.status.success() {
let stderr = String::from_utf8_lossy(&pull.stderr);
return Err(anyhow!("docker pull {image} failed: {}", stderr.trim()));
}
if let Some(net) = docker_network(spec) {
self.ensure_network(net).await?;
}
let args = Self::run_args(spec, mesh, &image, &name)?;
let argv: Vec<&str> = args.iter().map(String::as_str).collect();
let run = self
.cmd()
.args(&argv[..])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await
.with_context(|| format!("docker run {name}"))?;
if !run.status.success() {
let stderr = String::from_utf8_lossy(&run.stderr);
return Err(anyhow!("docker run {name} failed: {}", stderr.trim()));
}
let di = self
.inspect(&name)
.await?
.ok_or_else(|| anyhow!("container {name} not found after docker run"))?;
let container_id = di.id.clone();
let task_pid = di.state.host_pid().unwrap_or(0);
tracing::info!(
name = %name,
container_id = %container_id,
task_pid,
mesh_ip = %mesh.mesh_ip,
"docker workload deployed"
);
Ok(DeployResult {
container_id,
mesh_ip: mesh.mesh_ip,
task_pid,
})
}
async fn list_workloads(&self) -> Result<Vec<WorkloadState>> {
Ok(self
.list_workloads_detailed()
.await?
.into_iter()
.map(|w| w.state)
.collect())
}
async fn get_workload(&self, ident: &MeshIdent) -> Result<Option<WorkloadState>> {
let name = Self::container_name(ident);
match self.inspect(name).await? {
Some(di) => Ok(Some(Self::inspect_to_state(di, ident.clone()))),
None => Ok(None),
}
}
async fn stream_logs(&self, ident: &MeshIdent, opts: LogOpts) -> Result<LogStream> {
let name = Self::container_name(ident).to_string();
let ident_clone = ident.clone();
let mut args: Vec<String> = vec!["logs".into()];
match opts.tail {
Some(n) => {
args.push("--tail".into());
args.push(n.to_string());
}
None => {
args.push("--tail".into());
args.push("all".into());
}
}
args.push(name.clone());
let out = self
.cmd()
.args(args.iter().map(String::as_str))
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await
.with_context(|| format!("docker logs {name}"))?;
if !out.status.success() {
return Ok(Box::pin(tokio_stream::empty()));
}
let include_stdout = opts
.stream
.map(|s| s == LogStreamKind::Stdout)
.unwrap_or(true);
let include_stderr = opts
.stream
.map(|s| s == LogStreamKind::Stderr)
.unwrap_or(true);
let now_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let mut events: Vec<LogEvent> = Vec::new();
if include_stdout {
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
events.push(LogEvent {
timestamp_ms: now_ms,
ident: ident_clone.clone(),
stream: LogStreamKind::Stdout,
message: line.to_string(),
correlation_id: None,
});
}
}
if include_stderr {
let stderr = String::from_utf8_lossy(&out.stderr);
for line in stderr.lines().filter(|l| !l.trim().is_empty()) {
events.push(LogEvent {
timestamp_ms: now_ms,
ident: ident_clone.clone(),
stream: LogStreamKind::Stderr,
message: line.to_string(),
correlation_id: None,
});
}
}
Ok(Box::pin(tokio_stream::iter(events)))
}
async fn graceful_upgrade_workload(
&self,
spec: &WorkloadSpec,
mesh: &MeshAssignment,
) -> Result<DeployResult> {
tracing::warn!(
ident = %spec.expose.mesh.identity.0,
"Backend::Docker graceful_upgrade_workload performs a connection-dropping \
reload (re-deploy) — pingora zero-downtime fd-handoff is a containerd/native \
capability; the new cert goes live but existing connections are dropped"
);
self.deploy_workload(spec, mesh).await
}
async fn restart_workload(&self, ident: &MeshIdent) -> Result<()> {
let name = Self::container_name(ident);
let out = self
.cmd()
.args(["restart", name])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await
.with_context(|| format!("docker restart {name}"))?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(anyhow!("docker restart {name} failed: {}", stderr.trim()));
}
tracing::info!(name = %name, "docker workload restarted");
Ok(())
}
async fn teardown_workload(&self, ident: &MeshIdent) -> Result<()> {
let name = Self::container_name(ident);
let stop = self
.cmd()
.args(["stop", "-t", "5", name])
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()
.await
.with_context(|| format!("docker stop {name}"))?;
if !stop.status.success() {
let stderr = String::from_utf8_lossy(&stop.stderr);
if !is_missing_container(&stderr) {
return Err(anyhow!("docker stop {name} failed: {}", stderr.trim()));
}
}
let rm = self
.cmd()
.args(["rm", "-f", name])
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()
.await
.with_context(|| format!("docker rm {name}"))?;
if !rm.status.success() {
let stderr = String::from_utf8_lossy(&rm.stderr);
if !is_missing_container(&stderr) {
return Err(anyhow!("docker rm {name} failed: {}", stderr.trim()));
}
}
tracing::info!(name = %name, "docker workload torn down");
Ok(())
}
async fn health(&self) -> Result<RuntimeHealth> {
let out = self
.cmd()
.args(["version", "--format", "{{.Server.Version}}"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.await;
match out {
Ok(o) if o.status.success() => {
let version = String::from_utf8_lossy(&o.stdout).trim().to_string();
Ok(RuntimeHealth {
ok: true,
version: if version.is_empty() {
None
} else {
Some(version)
},
detail: None,
})
}
Ok(o) => {
let detail = String::from_utf8_lossy(&o.stderr).trim().to_string();
Ok(RuntimeHealth {
ok: false,
version: None,
detail: if detail.is_empty() {
None
} else {
Some(detail)
},
})
}
Err(e) => Ok(RuntimeHealth {
ok: false,
version: None,
detail: Some(e.to_string()),
}),
}
}
}
fn restart_flag(policy: &workload_spec::RestartPolicy) -> String {
use workload_spec::RestartPolicy as P;
match policy {
P::Always => "unless-stopped".into(),
P::OnFailure { max_attempts, .. } => format!("on-failure:{max_attempts}"),
P::Never => "no".into(),
}
}
fn parse_publish(spec: &WorkloadSpec) -> Result<Vec<(u16, u16)>> {
let Some(raw) = spec.annotations.get(PUBLISH_ANNOTATION) else {
return Ok(Vec::new());
};
let mut out = Vec::new();
for entry in raw.split(',').map(str::trim).filter(|e| !e.is_empty()) {
let (host, container) = entry.split_once(':').ok_or_else(|| {
anyhow!("{PUBLISH_ANNOTATION} entry {entry:?} is not `host:container`")
})?;
let host: u16 = host
.trim()
.parse()
.with_context(|| format!("{PUBLISH_ANNOTATION} host port in {entry:?}"))?;
let container: u16 = container
.trim()
.parse()
.with_context(|| format!("{PUBLISH_ANNOTATION} container port in {entry:?}"))?;
out.push((host, container));
}
Ok(out)
}
fn docker_network(spec: &WorkloadSpec) -> Option<&str> {
spec.annotations
.get(NETWORK_ANNOTATION)
.map(String::as_str)
.map(str::trim)
.filter(|n| !n.is_empty())
}
fn network_aliases(spec: &WorkloadSpec) -> Vec<&str> {
spec.annotations
.get(NETWORK_ALIAS_ANNOTATION)
.map(|raw| {
raw.split(',')
.map(str::trim)
.filter(|a| !a.is_empty())
.collect()
})
.unwrap_or_default()
}
fn map_docker_state(state: &DockerState, restart_count: u32) -> WorkloadStatus {
if state.restarting || state.status == "restarting" {
return WorkloadStatus::Restarting {
last_exit_code: state.exit_code,
restart_count,
last_finished_at_unix_ms: parse_docker_timestamp_ms(&state.finished_at),
};
}
match state.status.as_str() {
"running" => WorkloadStatus::Running,
"created" => WorkloadStatus::Pending,
"paused" | "removing" => WorkloadStatus::Stopping,
"exited" if state.exit_code == 0 => WorkloadStatus::Stopped,
"exited" => WorkloadStatus::Failed {
reason: format!("exited with code {}", state.exit_code),
},
"dead" => WorkloadStatus::Failed {
reason: "container is dead".into(),
},
other => WorkloadStatus::Failed {
reason: format!("unknown docker status: {other}"),
},
}
}
fn parse_docker_timestamp_ms(s: &str) -> u64 {
if s.is_empty() || s.starts_with("0001") {
return 0;
}
let s = s.trim_end_matches('Z');
let (date_part, time_part) = match s.split_once('T') {
Some(p) => p,
None => return 0,
};
let mut dp = date_part.splitn(3, '-');
let year: u64 = dp.next().and_then(|p| p.parse().ok()).unwrap_or(0);
let month: u64 = dp.next().and_then(|p| p.parse().ok()).unwrap_or(0);
let day: u64 = dp.next().and_then(|p| p.parse().ok()).unwrap_or(0);
let time_no_frac = time_part.split('.').next().unwrap_or("");
let mut tp = time_no_frac.splitn(3, ':');
let hour: u64 = tp.next().and_then(|p| p.parse().ok()).unwrap_or(0);
let min: u64 = tp.next().and_then(|p| p.parse().ok()).unwrap_or(0);
let sec: u64 = tp.next().and_then(|p| p.parse().ok()).unwrap_or(0);
if year < 1970 || month == 0 || month > 12 || day == 0 {
return 0;
}
let years_since_epoch = year - 1970;
let leap_days = years_since_epoch / 4;
let days_in_month: [u64; 13] = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let days_through_month: u64 = days_in_month[..month as usize].iter().sum::<u64>();
let day_of_year = days_through_month + day - 1;
let total_days = years_since_epoch * 365 + leap_days + day_of_year;
let total_secs = total_days * 86_400 + hour * 3_600 + min * 60 + sec;
total_secs * 1_000
}
fn is_missing_container(stderr: &str) -> bool {
let lower = stderr.to_ascii_lowercase();
lower.contains("no such container") || lower.contains("no such object")
}
#[cfg(test)]
mod tests {
use super::*;
use workload_spec::MeshIdent;
async fn docker_available() -> bool {
let out = Command::new("docker")
.args(["version"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await;
matches!(out, Ok(s) if s.success())
}
fn state(status: &str, restarting: bool, exit_code: i32) -> DockerState {
DockerState {
status: status.into(),
restarting,
exit_code,
finished_at: "2024-01-15T10:25:03.123456789Z".into(),
pid: if status == "running" { 4242 } else { 0 },
}
}
#[test]
fn restarting_flag_overrides_status() {
let ws = map_docker_state(&state("running", true, 137), 3);
match ws {
WorkloadStatus::Restarting {
last_exit_code,
restart_count,
last_finished_at_unix_ms,
} => {
assert_eq!(last_exit_code, 137);
assert_eq!(restart_count, 3);
assert!(last_finished_at_unix_ms > 0);
}
other => panic!("expected Restarting, got {other:?}"),
}
}
#[test]
fn restarting_status_string_matches() {
let s = DockerState {
status: "restarting".into(),
restarting: false, exit_code: 2,
finished_at: "0001-01-01T00:00:00Z".into(),
pid: 0,
};
let ws = map_docker_state(&s, 1);
assert!(
matches!(
ws,
WorkloadStatus::Restarting {
restart_count: 1,
last_exit_code: 2,
..
}
),
"got {ws:?}"
);
}
#[test]
fn running_maps_to_running() {
assert_eq!(
map_docker_state(&state("running", false, 0), 0),
WorkloadStatus::Running
);
}
#[test]
fn created_maps_to_pending() {
assert_eq!(
map_docker_state(&state("created", false, 0), 0),
WorkloadStatus::Pending
);
}
#[test]
fn paused_maps_to_stopping() {
assert_eq!(
map_docker_state(&state("paused", false, 0), 0),
WorkloadStatus::Stopping
);
}
#[test]
fn exited_zero_maps_to_stopped() {
assert_eq!(
map_docker_state(&state("exited", false, 0), 0),
WorkloadStatus::Stopped
);
}
#[test]
fn exited_nonzero_maps_to_failed() {
assert!(matches!(
map_docker_state(&state("exited", false, 1), 0),
WorkloadStatus::Failed { .. }
));
}
#[test]
fn dead_maps_to_failed() {
assert!(matches!(
map_docker_state(&state("dead", false, 0), 0),
WorkloadStatus::Failed { .. }
));
}
#[test]
fn running_container_reports_its_host_pid() {
assert_eq!(state("running", false, 0).host_pid(), Some(4242));
}
#[test]
fn zero_pid_means_no_running_process() {
assert_eq!(state("exited", false, 0).host_pid(), None);
assert_eq!(state("created", false, 0).host_pid(), None);
}
#[test]
fn negative_pid_is_rejected() {
let mut s = state("running", false, 0);
s.pid = -1;
assert_eq!(s.host_pid(), None);
}
#[test]
fn inspect_to_workload_carries_pid_and_label_ident() {
let di = DockerInspect {
id: "deadbeef".into(),
state: state("running", false, 0),
restart_count: 0,
config: DockerConfig {
labels: Some(HashMap::from([
("yah.ident".to_string(), "yah-marketing".to_string()),
("yah.mesh_ip".to_string(), "100.64.0.7".to_string()),
])),
},
};
let w = DockerRuntime::inspect_to_workload(di, MeshIdent("yah-marketing".into()));
assert_eq!(w.pid, Some(4242));
assert_eq!(w.state.status, WorkloadStatus::Running);
assert_eq!(w.state.container_id, "deadbeef");
assert_eq!(w.state.mesh_ip, Some("100.64.0.7".parse().unwrap()));
}
#[test]
fn workload_id_comes_from_its_label_not_the_identity() {
let di = DockerInspect {
id: "deadbeef".into(),
state: state("running", false, 0),
restart_count: 0,
config: DockerConfig {
labels: Some(HashMap::from([
("yah.ident".to_string(), "forge.abc".to_string()),
(WORKLOAD_ID_LABEL.to_string(), "forge-abc".to_string()),
])),
},
};
let w = DockerRuntime::inspect_to_workload(di, MeshIdent("forge.abc".into()));
assert_eq!(w.workload_id, "forge-abc");
assert_eq!(w.state.ident.0, "forge.abc");
}
#[test]
fn workload_id_falls_back_to_identity_when_label_absent() {
let di = DockerInspect {
id: "deadbeef".into(),
state: state("running", false, 0),
restart_count: 0,
config: DockerConfig {
labels: Some(HashMap::from([(
"yah.ident".to_string(),
"passway".to_string(),
)])),
},
};
let w = DockerRuntime::inspect_to_workload(di, MeshIdent("passway".into()));
assert_eq!(w.workload_id, "passway");
}
#[test]
fn ident_of_prefers_label_then_falls_back_to_name() {
let with_label = DockerInspect {
id: "a".into(),
state: state("running", false, 0),
restart_count: 0,
config: DockerConfig {
labels: Some(HashMap::from([(
"yah.ident".to_string(),
"forge.abc".to_string(),
)])),
},
};
assert_eq!(
DockerRuntime::ident_of(&with_label, "forge-abc").0,
"forge.abc"
);
let no_label = DockerInspect {
id: "b".into(),
state: state("running", false, 0),
restart_count: 0,
config: DockerConfig { labels: None },
};
assert_eq!(DockerRuntime::ident_of(&no_label, "orphan").0, "orphan");
}
fn test_spec(name: &str) -> WorkloadSpec {
use workload_spec::*;
WorkloadSpec {
schema_version: SchemaVersion::V1,
name: name.to_string(),
image: ImageRef {
registry: "docker.io".into(),
repository: "library/alpine".into(),
tag: "latest".into(),
digest: workload_spec::testing::test_digest(),
},
tier: TierTag("infra".into()),
tenant: TenantId::singleton(),
namespace: NamespaceId::singleton(),
replicas: 1,
command: None,
entrypoint: None,
workdir: None,
user: None,
env: vec![],
secrets: vec![],
volumes: vec![],
resources: ResourceLimits {
memory_mb: 64,
cpu_millis: 128,
ephemeral_storage_mb: 128,
},
depends_on: vec![],
healthcheck: None,
restart_policy: RestartPolicy::Never,
archetype: None,
stop_policy: StopPolicy {
signal: 15,
grace_period: Millis::from_secs(5),
},
expose: ExposeSpec {
mesh: MeshExpose {
identity: MeshIdent(name.to_string()),
ports: vec![],
allow_from: vec![],
},
public: None,
operator: None,
},
labels: Default::default(),
annotations: Default::default(),
}
}
fn render(spec: &WorkloadSpec) -> Vec<String> {
DockerRuntime::run_args(
spec,
&MeshAssignment::inlined("127.0.0.1".parse().unwrap()),
"alpine:latest",
&spec.expose.mesh.identity.0,
)
.expect("render")
}
fn flag_value<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
args.iter()
.position(|a| a == flag)
.and_then(|i| args.get(i + 1))
.map(String::as_str)
}
fn flag_values<'a>(args: &'a [String], flag: &str) -> Vec<&'a str> {
args.iter()
.enumerate()
.filter(|(_, a)| a.as_str() == flag)
.filter_map(|(i, _)| args.get(i + 1))
.map(String::as_str)
.collect()
}
#[test]
fn every_deploy_carries_a_restart_flag() {
let spec = test_spec("passway");
assert_eq!(flag_value(&render(&spec), "--restart"), Some("no"));
}
#[test]
fn always_renders_unless_stopped_not_always() {
use workload_spec::RestartPolicy;
assert_eq!(restart_flag(&RestartPolicy::Always), "unless-stopped");
}
#[test]
fn on_failure_carries_the_attempt_cap() {
use workload_spec::{BackoffPolicy, RestartPolicy};
let p = RestartPolicy::OnFailure {
max_attempts: 3,
backoff: BackoffPolicy {
initial_ms: 100,
max_ms: 1000,
multiplier: 2.0,
},
};
assert_eq!(restart_flag(&p), "on-failure:3");
}
#[test]
fn never_renders_no() {
use workload_spec::RestartPolicy;
assert_eq!(restart_flag(&RestartPolicy::Never), "no");
}
#[test]
fn nonzero_resource_limits_render() {
let args = render(&test_spec("capped"));
assert_eq!(flag_value(&args, "--memory"), Some("64m"));
assert_eq!(flag_value(&args, "--cpu-shares"), Some("131"));
}
#[test]
fn zero_resource_limits_are_omitted() {
let mut spec = test_spec("uncapped");
spec.resources.memory_mb = 0;
spec.resources.cpu_millis = 0;
let args = render(&spec);
assert!(flag_value(&args, "--memory").is_none(), "{args:?}");
assert!(flag_value(&args, "--cpu-shares").is_none(), "{args:?}");
}
#[test]
fn published_ports_render_in_declaration_order() {
let mut spec = test_spec("minio");
spec.annotations
.insert(PUBLISH_ANNOTATION.into(), "9000:9000, 9001:9001".into());
assert_eq!(
flag_values(&render(&spec), "-p"),
["9000:9000", "9001:9001"]
);
}
#[test]
fn no_publish_annotation_publishes_nothing() {
assert!(flag_values(&render(&test_spec("minio")), "-p").is_empty());
}
#[test]
fn malformed_publish_entry_is_an_error() {
let mut spec = test_spec("minio");
spec.annotations
.insert(PUBLISH_ANNOTATION.into(), "9000".into());
let err = DockerRuntime::run_args(
&spec,
&MeshAssignment::inlined("127.0.0.1".parse().unwrap()),
"alpine:latest",
"minio",
)
.unwrap_err()
.to_string();
assert!(err.contains("host:container"), "got {err}");
}
#[test]
fn network_and_aliases_render_together() {
let mut spec = test_spec("minio");
spec.annotations
.insert(NETWORK_ANNOTATION.into(), "yah-pond-svc-pond".into());
spec.annotations
.insert(NETWORK_ALIAS_ANNOTATION.into(), "minio,object-store".into());
let args = render(&spec);
assert_eq!(flag_value(&args, "--network"), Some("yah-pond-svc-pond"));
assert_eq!(
flag_values(&args, "--network-alias"),
["minio", "object-store"]
);
}
#[test]
fn aliases_without_a_network_are_dropped() {
let mut spec = test_spec("minio");
spec.annotations
.insert(NETWORK_ALIAS_ANNOTATION.into(), "minio".into());
let args = render(&spec);
assert!(flag_values(&args, "--network-alias").is_empty());
assert!(flag_value(&args, "--network").is_none());
}
#[test]
fn bind_named_and_tmpfs_volumes_each_render() {
use workload_spec::{VolumeMount, VolumeSource};
let mut spec = test_spec("minio");
spec.volumes = vec![
VolumeMount {
source: VolumeSource::Bind {
host_path: "/var/lib/pond/minio".into(),
},
target: "/data".into(),
read_only: false,
},
VolumeMount {
source: VolumeSource::Named {
name: "assets".into(),
},
target: "/assets".into(),
read_only: true,
},
VolumeMount {
source: VolumeSource::Tmpfs { size_mb: 64 },
target: "/scratch".into(),
read_only: false,
},
];
let args = render(&spec);
assert_eq!(
flag_values(&args, "-v"),
["/var/lib/pond/minio:/data", "assets:/assets:ro"]
);
assert_eq!(flag_values(&args, "--tmpfs"), ["/scratch:size=64m"]);
}
#[test]
fn spec_labels_render_sorted_after_identity_labels() {
let mut spec = test_spec("minio");
spec.labels.insert("zeta".into(), "1".into());
spec.labels.insert("alpha".into(), "2".into());
let args = render(&spec);
let labels = flag_values(&args, "--label");
assert_eq!(
labels,
[
"yah.ident=minio",
"yah.workload_id=minio",
"yah.mesh_ip=127.0.0.1",
"alpha=2",
"zeta=1",
]
);
}
#[test]
fn image_and_command_come_last() {
let mut spec = test_spec("minio");
spec.command = Some(vec!["server".into(), "/data".into()]);
spec.annotations
.insert(PUBLISH_ANNOTATION.into(), "9000:9000".into());
let args = render(&spec);
let image_at = args.iter().position(|a| a == "alpine:latest").unwrap();
assert_eq!(&args[image_at + 1..], ["server", "/data"]);
assert!(
args[..image_at].contains(&"-p".to_string()),
"options must precede the image: {args:?}"
);
}
#[test]
fn zero_timestamp_returns_zero() {
assert_eq!(parse_docker_timestamp_ms("0001-01-01T00:00:00Z"), 0);
assert_eq!(parse_docker_timestamp_ms(""), 0);
}
#[test]
fn known_timestamp_is_positive_and_in_range() {
let ms = parse_docker_timestamp_ms("2024-01-15T10:25:03Z");
assert!(ms > 1_000_000_000_000, "expected ms > 1e12, got {ms}");
assert!(ms < 2_000_000_000_000, "expected ms < 2e12, got {ms}");
}
#[test]
fn fractional_seconds_accepted() {
let ms = parse_docker_timestamp_ms("2024-01-15T10:25:03.123456789Z");
assert!(ms > 1_000_000_000_000, "got {ms}");
}
#[test]
fn missing_container_detection() {
assert!(is_missing_container("Error: No such container: foo"));
assert!(is_missing_container(
"Error response from daemon: No such object: bar"
));
assert!(!is_missing_container("Error: permission denied"));
}
#[tokio::test]
async fn health_returns_ok_when_docker_reachable() {
if !docker_available().await {
eprintln!("SKIP: docker not reachable");
return;
}
let rt = DockerRuntime::new();
let h = rt.health().await.unwrap();
assert!(h.ok, "expected healthy docker daemon: {:?}", h.detail);
assert!(h.version.is_some(), "expected version string");
}
#[tokio::test]
async fn get_workload_returns_none_for_nonexistent() {
if !docker_available().await {
eprintln!("SKIP: docker not reachable");
return;
}
let rt = DockerRuntime::new();
let ident = MeshIdent("r471-f3-nonexistent-sentinel".to_string());
let result = rt.get_workload(&ident).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn list_workloads_does_not_error() {
if !docker_available().await {
eprintln!("SKIP: docker not reachable");
return;
}
let rt = DockerRuntime::new();
let _ = rt.list_workloads().await.unwrap();
}
#[tokio::test]
async fn teardown_is_idempotent_for_missing_container() {
if !docker_available().await {
eprintln!("SKIP: docker not reachable");
return;
}
let rt = DockerRuntime::new();
let ident = MeshIdent("r471-f3-teardown-idempotent-sentinel".to_string());
rt.teardown_workload(&ident).await.unwrap();
}
}