mirror of
https://github.com/zerotier/eggshell.git
synced 2026-05-22 16:27:01 -07:00
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "eggshell"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
authors = ["Erik Hollensbe <linux@hollensbe.org>", "Adam Ierymenko <adam.ierymenko@zerotier.com>"]
|
||||
description = "Remove testing docker containers after this object goes away"
|
||||
homepage = "https://github.com/zerotier/eggshell"
|
||||
repository = "https://github.com/zerotier/eggshell"
|
||||
documentation = "https://github.com/zerotier/eggshell/blob/main/README.md"
|
||||
license = "BSD-3-Clause"
|
||||
readme = "README.md"
|
||||
keywords = ["docker", "testing", "containers", "zerotier"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bollard = ">= 0"
|
||||
thiserror = ">= 0"
|
||||
tokio = { version = ">= 0", features = ["full"] }
|
||||
async-trait = ">= 0"
|
||||
@@ -0,0 +1,26 @@
|
||||
Copyright 2021 ZeroTier, Inc.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,84 @@
|
||||
# EggShell (for rust): automatically destroy containers you create
|
||||
|
||||
EggShell automatically cleans up all the containers it creates when the struct is dropped. It uses the [bollard](https://crates.io/crates/bollard) docker toolkit, and [tokio](https://crates.io/crates/tokio) internally to manage the containers.
|
||||
|
||||
To utilize, simply create containers through it and wait for EggShell to go away. It will automatically trigger its `Drop` implementation which will destroy the containers.
|
||||
|
||||
Example that shows off counting (from the `examples/basic.rs` source):
|
||||
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), eggshell::Error> {
|
||||
let docker = Arc::new(tokio::sync::Mutex::new(
|
||||
bollard::Docker::connect_with_unix_defaults().unwrap(),
|
||||
));
|
||||
|
||||
let mut gs = eggshell::EggShell::new(docker.clone()).await?;
|
||||
|
||||
let count = docker
|
||||
.lock()
|
||||
.await
|
||||
.list_containers::<String>(None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len();
|
||||
|
||||
println!(
|
||||
"before: {} containers -- starting 10 postgres containers",
|
||||
count
|
||||
);
|
||||
|
||||
for num in 0..10 {
|
||||
gs.launch(
|
||||
&format!("test-{}", num),
|
||||
bollard::container::Config {
|
||||
image: Some("postgres:latest".to_string()),
|
||||
env: Some(vec!["POSTGRES_HOST_AUTH_METHOD=trust".to_string()]),
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let newcount = docker
|
||||
.lock()
|
||||
.await
|
||||
.list_containers::<String>(None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len();
|
||||
|
||||
println!(
|
||||
"before: {} containers, after: {} containers -- now dropping",
|
||||
count, newcount
|
||||
);
|
||||
|
||||
drop(gs);
|
||||
|
||||
let newcount = docker
|
||||
.lock()
|
||||
.await
|
||||
.list_containers::<String>(None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len();
|
||||
|
||||
println!(
|
||||
"after dropping: orig: {} containers, after: {} containers",
|
||||
count, newcount
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Author
|
||||
|
||||
Erik Hollensbe <erik.hollensbe@zerotier.com>
|
||||
|
||||
## License
|
||||
|
||||
BSD 3-Clause
|
||||
@@ -0,0 +1,66 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), eggshell::Error> {
|
||||
let docker = Arc::new(tokio::sync::Mutex::new(
|
||||
bollard::Docker::connect_with_unix_defaults().unwrap(),
|
||||
));
|
||||
|
||||
let mut gs = eggshell::EggShell::new(docker.clone()).await?;
|
||||
|
||||
let count = docker
|
||||
.lock()
|
||||
.await
|
||||
.list_containers::<String>(None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len();
|
||||
|
||||
println!(
|
||||
"before: {} containers -- starting 10 postgres containers",
|
||||
count
|
||||
);
|
||||
|
||||
for num in 0..10 {
|
||||
gs.launch(
|
||||
&format!("test-{}", num),
|
||||
bollard::container::Config {
|
||||
image: Some("postgres:latest".to_string()),
|
||||
env: Some(vec!["POSTGRES_HOST_AUTH_METHOD=trust".to_string()]),
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let newcount = docker
|
||||
.lock()
|
||||
.await
|
||||
.list_containers::<String>(None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len();
|
||||
|
||||
println!(
|
||||
"before: {} containers, after: {} containers -- now dropping",
|
||||
count, newcount
|
||||
);
|
||||
|
||||
drop(gs);
|
||||
|
||||
let newcount = docker
|
||||
.lock()
|
||||
.await
|
||||
.list_containers::<String>(None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len();
|
||||
|
||||
println!(
|
||||
"after dropping: orig: {} containers, after: {} containers",
|
||||
count, newcount
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
//!
|
||||
//! EggShell automatically cleans up all the containers it creates when the ship is destroyed. It
|
||||
//! uses the [bollard](https://crates.io/crates/bollard) docker toolkit, and
|
||||
//! [tokio](https://crates.io/crates/tokio) internally to manage the containers. To utilize,
|
||||
//! simply create containers through it and wait for EggShell to go away. It will automatically
|
||||
//! trigger its `Drop` implementation which will destroy the containers.
|
||||
//!
|
||||
//! EggShell requires a multi-threaded tokio async runtime to function. Please create one in your
|
||||
//! environment that tokio can "find" with the tokio::runtime::Handle::current() call.
|
||||
//!
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::{runtime::Handle, sync::Mutex};
|
||||
|
||||
use bollard::{
|
||||
container::{CreateContainerOptions, RemoveContainerOptions, StartContainerOptions},
|
||||
Docker,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
/// EggShell is basically a container for containers. When provided a docker client, it funnels
|
||||
/// all container create operations through it, allowing it to track what needs to be cleaned up.
|
||||
/// At drop() time (or when teardown() is called), these containers are cleaned up.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EggShell {
|
||||
docker: Arc<Mutex<Docker>>,
|
||||
containers: Vec<String>,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
/// Error is a top-level error enum for EggShell.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("error talking to docker: {0}")]
|
||||
Docker(DockerError),
|
||||
#[error("unknown error: {0}")]
|
||||
Generic(String),
|
||||
}
|
||||
|
||||
/// DockerError covers only errors that are from docker specifically.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DockerError {
|
||||
#[error("could not ping docker")]
|
||||
Ping,
|
||||
#[error("could not create container: {0}")]
|
||||
CreateContainer(String),
|
||||
#[error("could not start container: {0}")]
|
||||
StartContainer(String),
|
||||
#[error("could not delete container: {0}")]
|
||||
DeleteContainer(String),
|
||||
}
|
||||
|
||||
impl EggShell {
|
||||
/// Construct a new EggShell. When dropped or when teardown() is called, all containers
|
||||
/// launched through it will be reaped.
|
||||
pub async fn new(docker: Arc<Mutex<Docker>>) -> Result<Self, Error> {
|
||||
match docker.lock().await.ping().await {
|
||||
Ok(_) => {}
|
||||
Err(_) => return Err(Error::Docker(DockerError::Ping)),
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
docker,
|
||||
containers: Vec::new(),
|
||||
debug: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// set_debug turns off the teardown functionality for this EggShell, allowing you to debug
|
||||
/// its behavior. This disables the teardown() call which drop() relies on, so you must be
|
||||
/// ready to clean up your own containers before enabling this.
|
||||
pub fn set_debug(&mut self, debug: bool) {
|
||||
self.debug = debug;
|
||||
}
|
||||
|
||||
/// launch is the meat of EggShell and is how most of the work gets done. When provided with a
|
||||
/// name and container options, it will create and start that container, adding it to a
|
||||
/// registry of containers it is tracking. When drop() happens on the EggShell this container
|
||||
/// will be removed.
|
||||
pub async fn launch(
|
||||
&mut self,
|
||||
name: &str,
|
||||
container: bollard::container::Config<String>,
|
||||
start_options: Option<StartContainerOptions<String>>,
|
||||
) -> Result<(), Error> {
|
||||
match self
|
||||
.docker
|
||||
.lock()
|
||||
.await
|
||||
.create_container(Some(CreateContainerOptions { name }), container)
|
||||
.await
|
||||
{
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
return Err(Error::Docker(DockerError::CreateContainer(
|
||||
name.to_string(),
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.docker
|
||||
.lock()
|
||||
.await
|
||||
.start_container(name.clone(), start_options)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(_) => return Err(Error::Docker(DockerError::StartContainer(name.to_string()))),
|
||||
};
|
||||
|
||||
self.containers.push(name.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// teardown is what reaps the launch()'d containers. It is called as a part of drop() as well.
|
||||
pub async fn teardown(&self) -> Result<(), Error> {
|
||||
if !self.debug {
|
||||
for container in &self.containers {
|
||||
match self
|
||||
.docker
|
||||
.lock()
|
||||
.await
|
||||
.remove_container(
|
||||
&container,
|
||||
Some(RemoveContainerOptions {
|
||||
force: true,
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
return Err(Error::Docker(DockerError::DeleteContainer(e.to_string())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EggShell {
|
||||
/// Reaps all containers registered with launch().
|
||||
fn drop(&mut self) {
|
||||
tokio::task::block_in_place(move || Handle::current().block_on(self.teardown()).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
mod tests {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn basic() {
|
||||
use super::EggShell;
|
||||
use bollard::container::Config;
|
||||
use bollard::Docker;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
let docker = Arc::new(Mutex::new(Docker::connect_with_unix_defaults().unwrap()));
|
||||
|
||||
let res = EggShell::new(docker.clone()).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
let count = docker
|
||||
.lock()
|
||||
.await
|
||||
.list_containers::<String>(None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len();
|
||||
|
||||
let mut gs = res.unwrap();
|
||||
|
||||
for num in 0..10 {
|
||||
let res = gs
|
||||
.launch(
|
||||
&format!("test-{}", num),
|
||||
Config {
|
||||
image: Some("postgres:latest".to_string()),
|
||||
env: Some(vec!["POSTGRES_HOST_AUTH_METHOD=trust".to_string()]),
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(res.is_ok())
|
||||
}
|
||||
|
||||
let newcount = docker
|
||||
.lock()
|
||||
.await
|
||||
.list_containers::<String>(None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len();
|
||||
|
||||
assert!(newcount == count + 10);
|
||||
|
||||
drop(gs);
|
||||
|
||||
let newcount = docker
|
||||
.lock()
|
||||
.await
|
||||
.list_containers::<String>(None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len();
|
||||
|
||||
assert!(newcount == count);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user