blob: f5d224f6f14c9eb28ebf99470ddaadfdaaaba763 [file] [log] [blame]
extern crate tar;
use bollard::container::{Config, CreateContainerOptions, StartContainerOptions};
use bollard::exec::{CreateExecOptions, StartExecResults};
use bollard::Docker;
use bytes::{BufMut, BytesMut};
use futures_util::stream::StreamExt;
use futures_util::TryStreamExt;
use passwd::PasswdEntry;
use rand::Rng;
use std::collections::HashMap;
use std::io::Read;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DockerContainerGuardError {
#[error("Docker API error: {0}")]
DockerError(#[from] bollard::errors::Error),
#[error("File not found: {0}")]
FileNotFound(String),
#[error("Failed to extract file: {0}")]
FileExtractionFailed(#[from] std::io::Error),
#[error("Too many files in tarball")]
TooManyFilesInTarball,
#[error("Failed to parse password entry: {0}")]
FailedToParsePasswdEntry(#[from] passwd::PasswdEntryError),
#[error("User not found: {0}")]
UserNotFound(String),
}
#[derive(Debug)]
pub struct DockerContainerGuard {
pub id: String,
pub image: String,
docker: Docker,
}
impl DockerContainerGuard {
// Spawns a new container using Bollard.
//
// The container is automatically cleaned up when the guard goes out of scope.
pub async fn spawn(image_name: &str) -> Result<Self, DockerContainerGuardError> {
let docker = Docker::connect_with_local_defaults()?;
let container_name: String = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(10)
.map(char::from)
.collect();
docker
.create_image(
Some(bollard::image::CreateImageOptions {
from_image: image_name,
..Default::default()
}),
None,
None,
)
.try_collect::<Vec<_>>()
.await?;
let container = docker
.create_container(
Some(CreateContainerOptions {
name: container_name,
..Default::default()
}),
Config {
image: Some(image_name),
cmd: Some(vec!["sh"]),
tty: Some(true),
..Default::default()
},
)
.await?;
docker
.start_container(&container.id, None::<StartContainerOptions<String>>)
.await?;
Ok(Self {
id: container.id,
image: image_name.to_string(),
docker,
})
}
/// Executes a command inside the container using Bollard.
///
/// Returns the output as a String.
pub async fn exec(&self, cmd: Vec<&str>) -> Result<String, bollard::errors::Error> {
let exec_instance = self
.docker
.create_exec(
&self.id,
CreateExecOptions {
attach_stdout: Some(true),
attach_stderr: Some(true),
cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
..Default::default()
},
)
.await?;
let start_exec_result = self.docker.start_exec(&exec_instance.id, None).await?;
if let StartExecResults::Attached {
output: out_stream, ..
} = start_exec_result
{
let output = out_stream
.filter_map(|chunk| async {
match chunk {
Ok(bollard::container::LogOutput::StdOut { message })
| Ok(bollard::container::LogOutput::StdErr { message }) => {
Some(String::from_utf8_lossy(&message).to_string())
}
_ => None,
}
})
.fold(String::new(), |mut acc, item| async move {
acc.push_str(&item);
acc
})
.await;
return Ok(output);
}
Ok(String::new())
}
// Read a file from the container.
pub async fn read_file(&self, path: &str) -> Result<String, DockerContainerGuardError> {
let bytes = self
.docker
.download_from_container::<String>(
&self.id,
Some(bollard::container::DownloadFromContainerOptions { path: path.into() }),
)
.try_fold(BytesMut::new(), |mut bytes, b| async move {
bytes.put(b);
Ok(bytes)
})
.await?;
if bytes.len() == 0 {
return Err(DockerContainerGuardError::FileNotFound(path.into()));
}
for file in tar::Archive::new(&bytes[..]).entries()? {
let mut s = String::new();
file?.read_to_string(&mut s).unwrap();
return Ok(s);
}
Err(DockerContainerGuardError::FileNotFound(path.into()))
}
// Get a HashMap of all users in the container.
pub async fn get_users(
&self,
) -> Result<HashMap<String, PasswdEntry>, DockerContainerGuardError> {
let output = self.read_file("/etc/passwd").await?;
return output
.lines()
.map(|line| {
PasswdEntry::from_line(line)
.map(|entry| (entry.name.clone(), entry))
.map_err(DockerContainerGuardError::from)
})
.collect();
}
// Get a specific user from the container.
pub async fn get_user(&self, name: &str) -> Result<PasswdEntry, DockerContainerGuardError> {
let users = self.get_users().await?;
let user = users
.get(name)
.ok_or_else(|| DockerContainerGuardError::UserNotFound(name.into()))?;
Ok(user.clone())
}
}
impl Drop for DockerContainerGuard {
fn drop(&mut self) {
let docker = self.docker.clone();
let container_id = self.id.clone();
tokio::spawn(async move {
docker
.remove_container(
&container_id,
Some(bollard::container::RemoveContainerOptions {
force: true,
..Default::default()
}),
)
.await
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_container_exec() -> Result<(), DockerContainerGuardError> {
let guard = DockerContainerGuard::spawn("alpine:latest").await?;
let output = guard.exec(vec!["echo", "hello from container"]).await?;
assert!(output.contains("hello from container"));
Ok(())
}
#[tokio::test]
async fn test_container_read_file() -> Result<(), DockerContainerGuardError> {
let guard = DockerContainerGuard::spawn("alpine:latest").await?;
let file = guard.read_file("/usr/lib/os-release").await?;
assert!(file.len() > 0);
Ok(())
}
#[tokio::test]
async fn test_container_get_user() -> Result<(), DockerContainerGuardError> {
let guard = DockerContainerGuard::spawn("alpine:latest").await?;
let user = guard.get_user("root").await?;
assert_eq!(user.name, "root");
Ok(())
}
}