use std::collections::HashMap;
use bollard::models::{ContainerCreateBody, HostConfig};
use bollard::query_parameters::{
ListContainersOptions, RemoveContainerOptions, StartContainerOptions,
};
use super::{DockerClient, describe};
pub const DEBUG_LABEL: &str = "dev.ferrodock.debug-shell";
pub const TARGET_ROOT: &str = "/target";
pub const DEBUG_IMAGE: &str = "busybox:latest";
pub fn debug_image() -> String {
std::env::var("FERRODOCK_DEBUG_IMAGE")
.ok()
.filter(|i| !i.trim().is_empty())
.unwrap_or_else(|| DEBUG_IMAGE.to_owned())
}
impl DockerClient {
pub async fn start_debug_helper(&self, target: &str, image: &str) -> Result<String, String> {
let local = self.list_images().await?;
let have = local
.iter()
.any(|i| i.id == image || i.tags.iter().any(|t| t == image));
if !have {
self.pull(image, |_| {})
.await
.map_err(|e| format!("couldn't get {image} for the debug shell: {e}"))?;
}
let ns = format!("container:{target}");
let script = format!("ln -sfn /proc/1/root {TARGET_ROOT}; exec sleep 86400");
let body = ContainerCreateBody {
image: Some(image.to_owned()),
entrypoint: Some(vec!["sh".into(), "-c".into(), script]),
cmd: Some(Vec::new()),
labels: Some(HashMap::from([(DEBUG_LABEL.to_owned(), target.to_owned())])),
host_config: Some(HostConfig {
pid_mode: Some(ns.clone()),
network_mode: Some(ns),
cap_add: Some(vec!["SYS_PTRACE".into()]),
auto_remove: Some(true),
..Default::default()
}),
..Default::default()
};
let created = self
.docker
.create_container(
None::<bollard::query_parameters::CreateContainerOptions>,
body,
)
.await
.map_err(describe)?;
if let Err(e) = self
.docker
.start_container(&created.id, None::<StartContainerOptions>)
.await
{
let _ = self.remove_debug_helper(&created.id).await;
return Err(describe(e));
}
Ok(created.id)
}
pub async fn remove_debug_helper(&self, id: &str) -> Result<(), String> {
let opts = RemoveContainerOptions {
force: true,
..Default::default()
};
self.docker
.remove_container(id, Some(opts))
.await
.map_err(describe)
}
pub async fn remove_debug_helpers(&self) -> Result<(), String> {
let opts = ListContainersOptions {
all: true,
filters: Some(HashMap::from([(
"label".to_owned(),
vec![DEBUG_LABEL.to_owned()],
)])),
..Default::default()
};
let list = self
.docker
.list_containers(Some(opts))
.await
.map_err(describe)?;
for id in list.into_iter().filter_map(|c| c.id) {
self.remove_debug_helper(&id).await?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore]
async fn live_daemon_debugs_a_container() {
use futures_util::StreamExt;
use tokio::io::AsyncWriteExt;
let client = DockerClient::connect().unwrap();
let Some(image) = client.list_images().await.unwrap().into_iter().next() else {
return;
};
let spec = crate::docker::RunSpec {
image: image.id.clone(),
name: "ferrodock-debug-target".into(),
command: "sleep 4321".into(),
..Default::default()
};
let opts = || RemoveContainerOptions {
force: true,
..Default::default()
};
let _ = client
.docker
.remove_container(&spec.name, Some(opts()))
.await;
let target = client.run(&spec).await.unwrap();
let helper = client.start_debug_helper(&target, &image.id).await.unwrap();
let mut sh = client.open_shell(&helper, "", 120, 30).await.unwrap();
sh.input
.write_all(b"ps; ls /target/; echo done-$((1+1))\r")
.await
.unwrap();
let mut screen = vt100::Parser::new(30, 120, 0);
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(10);
while !screen.screen().contents().contains("done-2") {
match tokio::time::timeout_at(deadline, sh.output.next()).await {
Ok(Some(Ok(out))) => screen.process(&out.into_bytes()),
_ => break,
}
}
let shown = screen.screen().contents();
let hidden = client
.list_containers()
.await
.unwrap()
.iter()
.all(|c| c.id != helper);
client.remove_debug_helper(&helper).await.unwrap();
client
.docker
.remove_container(&target, Some(opts()))
.await
.unwrap();
assert!(
shown.contains("sleep 4321"),
"sees the target's processes:\n{shown}"
);
assert!(shown.contains("etc"), "sees the target's files:\n{shown}");
assert!(hidden, "helpers don't show in the container list");
}
}