use crate::container::OperationalContainer;
use crate::docker::Docker;
use crate::dockertest::Network;
use crate::engine::{bootstrap, Debris, Engine, Orbiting};
use crate::static_container::SCOPED_NETWORKS;
use crate::utils::generate_random_string;
use crate::{DockerTest, DockerTestError};
use futures::future::Future;
use std::any::Any;
use std::clone::Clone;
use std::collections::HashMap;
use std::panic;
use tracing::{error, event, trace, Level};
pub(crate) struct Runner {
client: Docker,
config: DockerTest,
named_volumes: Vec<String>,
network: String,
pub(crate) id: String,
}
#[derive(Clone)]
pub struct DockerOperations {
engine: Engine<Orbiting>,
}
enum PruneStrategy {
RunningRegardless,
RunningOnFailure,
StopOnFailure,
RemoveRegardless,
}
impl DockerOperations {
fn try_handle<'a>(
&'a self,
handle: &'a str,
) -> Result<&'a OperationalContainer, DockerTestError> {
if self.engine.handle_collision(handle) {
return Err(DockerTestError::TestBody(format!(
"handle '{}' defined multiple times",
handle
)));
}
self.engine.resolve_handle(handle).ok_or_else(|| {
DockerTestError::TestBody(format!("container with handle '{}' not found", handle))
})
}
pub fn handle<'a>(&'a self, handle: &'a str) -> &'a OperationalContainer {
event!(Level::DEBUG, "requesting handle '{}", handle);
match self.try_handle(handle) {
Ok(h) => h,
Err(e) => {
event!(Level::ERROR, "{}", e.to_string());
panic!("{}", e);
}
}
}
pub fn failure(&self, msg: &str) {
event!(Level::ERROR, "test failure: {}", msg);
panic!("test failure: {}", msg);
}
}
impl Runner {
pub async fn new(config: DockerTest) -> Runner {
Self::try_new(config).await.unwrap()
}
pub async fn try_new(config: DockerTest) -> Result<Runner, DockerTestError> {
let client = Docker::new()?;
let id = generate_random_string(20);
let network = match &config.network {
Network::External(n) => n.clone(),
Network::Isolated => format!("dockertest-rs-{}", id),
Network::Singular => {
SCOPED_NETWORKS
.create_singular_network(
&client,
own_container_id().as_deref(),
&config.namespace,
)
.await?
}
};
Ok(Runner {
client,
named_volumes: Vec::new(),
network,
id,
config,
})
}
pub async fn run_impl<T, Fut>(mut self, test: T) -> Result<(), DockerTestError>
where
T: FnOnce(DockerOperations) -> Fut,
Fut: Future<Output = ()> + Send + 'static,
{
self.check_if_inside_container();
self.resolve_named_volumes().await?;
let compositions = std::mem::take(&mut self.config.compositions);
let mut engine = bootstrap(compositions);
engine.resolve_final_container_name(&self.config.namespace);
let mut engine = engine.fuel();
engine.resolve_inject_container_name_env()?;
engine
.pull_images(&self.client, &self.config.default_source)
.await?;
self.resolve_network().await?;
let engine = match engine
.ignite(&self.client, &self.network, &self.config.network)
.await
{
Ok(e) => e,
Err(engine) => {
let mut creation_failures = engine.creation_failures();
let total = creation_failures.len();
creation_failures.iter().enumerate().for_each(|(i, e)| {
trace!("container {} of {} creation failures: {}", i + 1, total, e);
});
let engine = engine.decommission();
if let Err(errors) = engine.handle_startup_logs().await {
for err in errors {
error!("{err}");
}
}
self.teardown(engine, false).await;
return Err(creation_failures
.pop()
.expect("dockertest bug: cleanup path expected container creation error"));
}
};
let mut engine = match engine.orbiting().await {
Ok(e) => e,
Err((engine, e)) => {
let engine = engine.decommission();
if let Err(errors) = engine.handle_startup_logs().await {
for err in errors {
error!("{err}");
}
}
self.teardown(engine, false).await;
return Err(e);
}
};
let network_name = match self.config.network {
Network::Singular => SCOPED_NETWORKS.name(&self.config.namespace),
Network::External(_) | Network::Isolated => self.network.clone(),
};
if let Err(mut errors) = engine.inspect(&self.client, &network_name).await {
let total = errors.len();
errors.iter().enumerate().for_each(|(i, e)| {
trace!("container {} of {} inspect failures: {}", i + 1, total, e);
});
let engine = engine.decommission();
self.teardown(engine, false).await;
return Err(errors
.pop()
.expect("dockertest bug: cleanup path expected container inspect error"));
};
let ops = DockerOperations {
engine: engine.clone(),
};
let result: Result<(), Option<Box<dyn Any + Send + 'static>>> =
match tokio::spawn(test(ops)).await {
Ok(_) => {
event!(Level::DEBUG, "test body success");
Ok(())
}
Err(e) => {
event!(
Level::DEBUG,
"test body failed (cancelled: {}, panicked: {})",
e.is_cancelled(),
e.is_panic()
);
Err(e.try_into_panic().ok())
}
};
let engine = engine.decommission();
if let Err(errors) = engine.handle_logs(result.is_err()).await {
for err in errors {
error!("{err}");
}
}
self.teardown(engine, result.is_err()).await;
if let Err(option) = result {
match option {
Some(panic) => panic::resume_unwind(panic),
None => panic!("test future cancelled"),
}
}
Ok(())
}
fn check_if_inside_container(&mut self) {
if let Some(id) = own_container_id() {
event!(
Level::TRACE,
"dockertest container id env is set, we are running inside a container, id: {}",
id
);
self.config.container_id = Some(id);
} else {
event!(
Level::TRACE,
"dockertest container id env is not set, running native on host"
);
}
}
async fn resolve_network(&self) -> Result<(), DockerTestError> {
match &self.config.network {
Network::Singular | Network::External(_) => Ok(()),
Network::Isolated => {
self.client
.create_network(&self.network, self.config.container_id.as_deref())
.await
}
}
}
async fn teardown(&self, engine: Engine<Debris>, test_failed: bool) {
engine
.disconnect_static_containers(&self.client, &self.network, &self.config.network)
.await;
match env_prune_strategy() {
PruneStrategy::RunningRegardless => {
event!(
Level::DEBUG,
"Leave all containers running regardless of outcome"
);
}
PruneStrategy::RunningOnFailure if test_failed => {
event!(
Level::DEBUG,
"Leaving all containers running due to test failure"
);
}
PruneStrategy::StopOnFailure if test_failed => {
self.client
.stop_containers(engine.cleanup_containers())
.await;
self.teardown_network().await;
}
PruneStrategy::StopOnFailure
| PruneStrategy::RunningOnFailure
| PruneStrategy::RemoveRegardless => {
event!(Level::DEBUG, "forcefully removing all containers");
self.client
.remove_containers(engine.cleanup_containers())
.await;
self.teardown_network().await;
self.client.remove_volumes(&self.named_volumes).await;
}
}
}
async fn resolve_named_volumes(&mut self) -> Result<(), DockerTestError> {
let mut volume_name_map: HashMap<String, String> = HashMap::new();
let suffix = self.id.clone();
self.config.compositions.iter_mut().for_each(|c| {
let mut volume_names_with_path: Vec<String> = Vec::new();
c.named_volumes.iter().for_each(|(id, path)| {
if let Some(suffixed_name) = volume_name_map.get(id) {
volume_names_with_path.push(format!("{}:{}", &suffixed_name, &path));
} else {
let volume_name_with_path = format!("{}-{}:{}", id, &suffix, path);
volume_names_with_path.push(volume_name_with_path);
let suffixed_volume_name = format!("{}-{}", id, &suffix);
volume_name_map.insert(id.to_string(), suffixed_volume_name);
}
});
c.final_named_volume_names = volume_names_with_path;
});
self.named_volumes = volume_name_map.drain().map(|(_k, v)| v).collect();
event!(
Level::DEBUG,
"added named volumes to cleanup list: {:?}",
&self.named_volumes
);
Ok(())
}
async fn teardown_network(&self) {
match self.config.network {
Network::Singular => (),
Network::External(_) => (),
Network::Isolated => {
self.client
.delete_network(&self.network, self.config.container_id.as_deref())
.await
}
}
}
}
fn own_container_id() -> Option<String> {
std::env::var("DOCKERTEST_CONTAINER_ID_INJECT_TO_NETWORK").ok()
}
fn env_prune_strategy() -> PruneStrategy {
match std::env::var_os("DOCKERTEST_PRUNE") {
Some(val) => match val.to_string_lossy().to_lowercase().as_str() {
"stop_on_failure" => PruneStrategy::StopOnFailure,
"never" => PruneStrategy::RunningRegardless,
"running_on_failure" => PruneStrategy::RunningOnFailure,
"always" => PruneStrategy::RemoveRegardless,
_ => {
event!(Level::WARN, "unrecognized `DOCKERTEST_PRUNE = {:?}`", val);
event!(Level::DEBUG, "defaulting to prune stategy RemoveRegardless");
PruneStrategy::RemoveRegardless
}
},
None => PruneStrategy::RemoveRegardless,
}
}