use std::io::Cursor;
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt as _;
#[cfg(windows)]
use std::os::windows::process::ExitStatusExt as _;
use std::path::PathBuf;
use std::process::ExitStatus;
use bollard::Docker;
use bollard::body_full;
use bollard::container::LogOutput;
use bollard::query_parameters::AttachContainerOptions;
use bollard::query_parameters::InspectContainerOptions;
use bollard::query_parameters::RemoveContainerOptions;
use bollard::query_parameters::StartContainerOptions;
use bollard::query_parameters::UploadToContainerOptions;
use bollard::query_parameters::WaitContainerOptions;
use bollard::secret::ContainerWaitResponse;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use tokio_stream::StreamExt as _;
use tracing::debug;
use tracing::info;
use crate::Error;
use crate::Result;
mod builder;
pub use builder::Builder;
const DEFAULT_TAR_CAPACITY: usize = 0xFFFF;
pub struct Container {
client: Docker,
id: String,
stdout: Option<PathBuf>,
stderr: Option<PathBuf>,
}
impl Container {
pub fn new(
client: Docker,
id: String,
stdout: Option<PathBuf>,
stderr: Option<PathBuf>,
) -> Self {
Self {
client,
id,
stdout,
stderr,
}
}
pub async fn upload_file(&self, path: &str, contents: &[u8]) -> Result<()> {
let mut tar = tar::Builder::new(Vec::with_capacity(DEFAULT_TAR_CAPACITY));
let path = path.trim_start_matches("/");
let mut header = tar::Header::new_gnu();
header.set_path(path).unwrap();
header.set_size(contents.len() as u64);
header.set_mode(0o644);
tar.append_data(&mut header, path, Cursor::new(contents))
.unwrap();
self.client
.upload_to_container(
&self.id,
Some(UploadToContainerOptions {
path: String::from("/"),
..Default::default()
}),
body_full(tar.into_inner().unwrap().into()),
)
.await
.map_err(Error::Docker)
}
pub async fn run(&self, name: &str, started: impl FnOnce()) -> Result<ExitStatus> {
let stream = if self.stdout.is_some() || self.stderr.is_some() {
debug!(
"attaching to container `{id}` (task `{name}`)",
id = self.id
);
Some(
self.client
.attach_container(
&self.id,
Some(AttachContainerOptions {
stdout: self.stdout.is_some(),
stderr: self.stderr.is_some(),
stream: true,
..Default::default()
}),
)
.await
.map_err(Error::Docker)?
.output,
)
} else {
None
};
info!("starting container `{id}` (task `{name}`)", id = self.id);
self.client
.start_container(&self.id, None::<StartContainerOptions>)
.await
.map_err(Error::Docker)?;
started();
info!("container `{id}` (task `{name}`) has started", id = self.id);
if self.stdout.is_some() || self.stderr.is_some() {
let mut stdout = match &self.stdout {
Some(path) => Some(File::create(path).await.map_err(|e| {
Error::Message(format!(
"failed to create stdout file `{path}`: {e}",
path = path.display()
))
})?),
None => None,
};
let mut stderr = match &self.stderr {
Some(path) => Some(File::create(path).await.map_err(|e| {
Error::Message(format!(
"failed to create stderr file `{path}`: {e}",
path = path.display()
))
})?),
None => None,
};
let mut stream = stream.expect("should have attached to the container");
while let Some(result) = stream.next().await {
let output = result.map_err(Error::Docker)?;
match output {
LogOutput::StdOut { message } => {
stdout
.as_mut()
.unwrap()
.write(&message)
.await
.map_err(|e| {
Error::Message(format!(
"failed to write to stdout file `{path}`: {e}",
path = self.stdout.as_ref().unwrap().display()
))
})?;
}
LogOutput::StdErr { message } => {
stderr
.as_mut()
.unwrap()
.write(&message)
.await
.map_err(|e| {
Error::Message(format!(
"failed to write to stderr file `{path}`: {e}",
path = self.stderr.as_ref().unwrap().display()
))
})?;
}
_ => {}
}
}
}
debug!(
"waiting for container `{id}` (task `{name}`) to exit",
id = self.id
);
let mut wait_stream = self
.client
.wait_container(&self.id, None::<WaitContainerOptions>);
let mut exit_code = None;
if let Some(result) = wait_stream.next().await {
match result {
Ok(ContainerWaitResponse {
status_code: code, ..
})
| Err(bollard::errors::Error::DockerContainerWaitError { code, .. }) => {
exit_code = Some(code);
}
Err(e) => return Err(e.into()),
}
}
if exit_code.is_none() {
let container = self
.client
.inspect_container(&self.id, None::<InspectContainerOptions>)
.await
.map_err(Error::Docker)?;
exit_code = Some(
container
.state
.expect("Docker reported a container without a state")
.exit_code
.expect("Docker reported a finished contained without an exit code"),
);
}
#[cfg(unix)]
let status = ExitStatus::from_raw((exit_code.unwrap() as i32) << 8);
#[cfg(windows)]
let status = ExitStatus::from_raw(exit_code.unwrap() as u32);
info!(
"container `{id}` (task `{name}`) has exited with {status}",
id = self.id
);
Ok(status)
}
async fn remove_inner(&self, force: bool) -> Result<()> {
self.client
.remove_container(
&self.id,
Some(RemoveContainerOptions {
force,
..Default::default()
}),
)
.await
.map_err(Error::Docker)?;
Ok(())
}
pub async fn remove(&self) -> Result<()> {
debug!("removing container `{id}`", id = self.id);
self.remove_inner(false).await
}
pub async fn force_remove(&self) -> Result<()> {
debug!("force removing container `{id}`", id = self.id);
self.remove_inner(true).await
}
}