Skip to content

Instantly share code, notes, and snippets.

@luke-biel
Created December 4, 2022 22:33
Show Gist options
  • Save luke-biel/19da67991c326a83e90f1141754a7199 to your computer and use it in GitHub Desktop.
Save luke-biel/19da67991c326a83e90f1141754a7199 to your computer and use it in GitHub Desktop.
Unholy static docker containers with teardown on app exit
use bollard::container::{CreateContainerOptions, RemoveContainerOptions, StopContainerOptions, WaitContainerOptions};
use bollard::{container, Docker};
use futures_util::{pin_mut, StreamExt};
use once_cell::sync::Lazy;
use static_init::destructor;
use std::collections::hash_map::{DefaultHasher, Entry};
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use tokio::runtime::Runtime;
static DOCKER_SYNC: Lazy<spin::Mutex<DockerSync>> = Lazy::new(|| spin::Mutex::new(DockerSync::setup()));
struct DockerSync {
docker: Docker,
cache: HashMap<ContainerOpts, String>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ContainerOpts {
image: &'static str,
exposed_ports: Vec<(u32, u32)>,
env: Vec<&'static str>,
cmd: &'static str,
}
#[destructor(0)]
extern "C" fn teardown() {
let runtime = Runtime::new().unwrap();
let sync = { DOCKER_SYNC.lock().cache.values().cloned().collect::<Vec<_>>() };
let docker = Docker::connect_with_local_defaults().unwrap();
for id in sync {
runtime.block_on(async {
docker
.stop_container(&id, Some(StopContainerOptions { t: 10 }))
.await
.unwrap();
let stream = docker.wait_container(
&id,
Some(WaitContainerOptions {
condition: "not-running",
}),
);
pin_mut!(stream);
while let Some(result) = stream.next().await {
if let Err(e) = result {
panic!("Error waiting for container: {}", e);
}
}
docker
.remove_container(
&id,
Some(RemoveContainerOptions {
force: true,
..Default::default()
}),
)
.await
.unwrap();
});
}
}
impl DockerSync {
fn setup() -> Self {
let docker = Docker::connect_with_local_defaults().unwrap();
Self {
docker,
cache: HashMap::new(),
}
}
}
pub async fn with_container<Fun, Ret>(create_opts: ContainerOpts, run: Fun) -> anyhow::Result<Ret>
where
Fun: FnOnce(&str) -> Ret,
{
let name = {
let mut sync = DOCKER_SYNC.lock();
let DockerSync { docker, cache } = &mut *sync;
match cache.entry(create_opts.clone()) {
Entry::Occupied(entry) => entry.get().to_string(),
Entry::Vacant(vacancy) => {
let mut hasher = DefaultHasher::new();
create_opts.hash(&mut hasher);
let hash = hasher.finish();
let name = format!("test-container-{}", hash);
docker
.create_container(
Some(CreateContainerOptions { name: &name }),
container::Config {
exposed_ports: None,
env: Some(create_opts.env.iter().map(|env| env.to_string()).collect()),
cmd: None,
image: Some(create_opts.image.to_string()),
..Default::default()
},
)
.await
.unwrap();
docker.start_container::<String>(&name, None).await.unwrap();
vacancy.insert(name).to_string()
}
}
};
Ok(run(&name))
}
#[tokio::test]
async fn validate() {
let container = ContainerOpts {
image: "postgres:latest",
exposed_ports: vec![(5432, 5432)],
env: vec!["POSTGRES_PASSWORD=1234"],
cmd: "postgres",
};
with_container(container, |c| {
eprintln!("Hello world {}", c);
assert!(true);
})
.await
.unwrap();
}
#[tokio::test]
async fn validate1() {
let container = ContainerOpts {
image: "postgres:latest",
exposed_ports: vec![(5432, 5432)],
env: vec!["POSTGRES_PASSWORD=1234"],
cmd: "postgres",
};
with_container(container, |c| {
eprintln!("Hello world, {}", c);
assert!(false);
})
.await
.unwrap();
}
#[tokio::test]
async fn validate2() {
let container = ContainerOpts {
image: "postgres:latest",
exposed_ports: vec![(5432, 5432)],
env: vec!["POSTGRES_PASSWORD=1235"],
cmd: "postgres",
};
with_container(container, |c| {
eprintln!("Hello world {}", c);
assert!(true);
})
.await
.unwrap();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment