#![cfg(all(unix, feature = "docker"))]
use std::io::{Read, Write};
use std::os::unix::net::UnixListener;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use foundation_core::valtron::valtron_test;
use foundation_deployment_docker::DockerClient;
use foundation_deployment_docker::generated::top::ContainerTopResponse;
fn temp_socket_path(tag: &str) -> PathBuf {
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!("ewe_docker_lc_{tag}_{pid}_{nanos}.sock"));
let _ = std::fs::remove_file(&path);
path
}
fn route(method: &str, path: &str) -> (u16, String) {
let path_no_query = path.split('?').next().unwrap_or(path);
let p = path_no_query.split_once("/v1.").map_or(path_no_query, |(_, rest)| {
rest.split_once('/').map_or(rest, |(_, r)| r)
});
match (method, p) {
("POST", p) if p.starts_with("containers/") && p.ends_with("/restart") => {
(204, String::new())
}
("POST", p) if p.starts_with("containers/") && p.contains("/rename") => {
(204, String::new())
}
("GET", p) if p.starts_with("containers/") && p.ends_with("/top") => {
let body = r#"{"Processes":[["root","1","0.0","0.0","123456","?","Ss","12:34","sleep 30"]],"Titles":["USER","PID","%CPU","%MEM","VSZ","RSS","TTY","STAT","START","COMMAND"]}"#;
(200, body.to_string())
}
("GET", "containers/json") => {
let body = r#"[{"Id":"abc123","Names":["/test_container"],"Image":"alpine:latest","ImageID":"sha256:abc","Command":"sleep 30","Created":1234567890,"Ports":[],"Labels":{},"State":"running","Status":"Up 1 hour","HostConfig":{"NetworkMode":"default"},"NetworkSettings":{"Networks":{"bridge":{"IPAddress":"172.17.0.2"}}},"Mounts":[]}]"#;
(200, body.to_string())
}
_ => (404, format!("{{\"message\":\"no route for {method} {p}\"}}")),
}
}
fn mock_daemon(socket_path: PathBuf, stop: Arc<AtomicBool>) {
let listener = UnixListener::bind(&socket_path).expect("bind mock docker socket");
listener
.set_nonblocking(true)
.expect("set nonblocking");
std::thread::spawn(move || {
while !stop.load(Ordering::Relaxed) {
match listener.accept() {
Ok((mut stream, _)) => {
let mut buf = [0u8; 8192];
let n = stream.read(&mut buf).unwrap_or(0);
let req = String::from_utf8_lossy(&buf[..n]);
let first = req.lines().next().unwrap_or("");
let mut parts = first.split_whitespace();
let method = parts.next().unwrap_or("");
let path = parts.next().unwrap_or("");
let (status, body) = route(method, path);
let reason = if status == 204 { "No Content" } else { "OK" };
let response = format!(
"HTTP/1.1 {status} {reason}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = stream.write_all(response.as_bytes());
let _ = stream.flush();
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::yield_now();
}
Err(_) => break,
}
}
});
}
#[valtron_test]
async fn docker_restart_container() {
let socket = temp_socket_path("restart");
let stop = Arc::new(AtomicBool::new(false));
mock_daemon(socket.clone(), stop.clone());
let client = DockerClient::connect_unix(&socket);
client
.restart_container("abc123", Some("SIGHUP"), Some(10))
.await
.expect("restart_container should succeed");
stop.store(true, Ordering::Relaxed);
let _ = std::fs::remove_file(&socket);
}
#[valtron_test]
async fn docker_restart_container_defaults() {
let socket = temp_socket_path("restart_def");
let stop = Arc::new(AtomicBool::new(false));
mock_daemon(socket.clone(), stop.clone());
let client = DockerClient::connect_unix(&socket);
client
.restart_container("abc123", None, None)
.await
.expect("restart_container with defaults should succeed");
stop.store(true, Ordering::Relaxed);
let _ = std::fs::remove_file(&socket);
}
#[valtron_test]
async fn docker_rename_container() {
let socket = temp_socket_path("rename");
let stop = Arc::new(AtomicBool::new(false));
mock_daemon(socket.clone(), stop.clone());
let client = DockerClient::connect_unix(&socket);
client
.rename_container("abc123", "new_name")
.await
.expect("rename_container should succeed");
stop.store(true, Ordering::Relaxed);
let _ = std::fs::remove_file(&socket);
}
#[valtron_test]
async fn docker_container_top() {
let socket = temp_socket_path("top");
let stop = Arc::new(AtomicBool::new(false));
mock_daemon(socket.clone(), stop.clone());
let client = DockerClient::connect_unix(&socket);
let top = client
.container_top("abc123", Some("aux"))
.await
.expect("container_top should succeed");
let titles = top.titles.expect("should have Titles");
assert_eq!(titles.len(), 10);
assert_eq!(titles[0], "USER");
assert_eq!(titles[9], "COMMAND");
let processes = top.processes.expect("should have Processes");
assert_eq!(processes.len(), 1);
assert_eq!(processes[0].len(), 9);
assert_eq!(processes[0][8], "sleep 30");
stop.store(true, Ordering::Relaxed);
let _ = std::fs::remove_file(&socket);
}
#[valtron_test]
async fn docker_container_top_no_ps_args() {
let socket = temp_socket_path("top_nops");
let stop = Arc::new(AtomicBool::new(false));
mock_daemon(socket.clone(), stop.clone());
let client = DockerClient::connect_unix(&socket);
let top = client
.container_top("abc123", None)
.await
.expect("container_top without ps_args should succeed");
assert!(top.titles.is_some());
stop.store(true, Ordering::Relaxed);
let _ = std::fs::remove_file(&socket);
}
#[valtron_test]
async fn docker_container_list() {
let socket = temp_socket_path("list");
let stop = Arc::new(AtomicBool::new(false));
mock_daemon(socket.clone(), stop.clone());
let client = DockerClient::connect_unix(&socket);
let containers = client
.container_list(true, None, false, None)
.await
.expect("container_list should succeed");
let arr = containers.as_array().expect("should be an array");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["Id"].as_str(), Some("abc123"));
assert_eq!(arr[0]["Names"][0].as_str(), Some("/test_container"));
stop.store(true, Ordering::Relaxed);
let _ = std::fs::remove_file(&socket);
}
#[valtron_test]
async fn docker_container_list_with_filters() {
let socket = temp_socket_path("list_filt");
let stop = Arc::new(AtomicBool::new(false));
mock_daemon(socket.clone(), stop.clone());
let client = DockerClient::connect_unix(&socket);
let containers = client
.container_list(false, Some(5), true, Some(r#"{"status":["running"]}"#))
.await
.expect("container_list with filters should succeed");
assert!(containers.is_array());
stop.store(true, Ordering::Relaxed);
let _ = std::fs::remove_file(&socket);
}
#[test]
fn container_top_response_deserializes_pascalcase_json() {
let json = r#"{"Processes":[["root","1"]],"Titles":["USER","PID"]}"#;
let resp: ContainerTopResponse =
serde_json::from_str(json).expect("deserialize ContainerTopResponse");
assert_eq!(resp.titles.as_deref(), Some(&["USER".to_string(), "PID".to_string()][..]));
let procs = resp.processes.expect("should have Processes");
assert_eq!(procs.len(), 1);
assert_eq!(procs[0][0], "root");
}