hello, eggshell

Signed-off-by: Erik Hollensbe <linux@hollensbe.org>
This commit is contained in:
Erik Hollensbe
2021-08-29 16:25:28 -07:00
commit 6a68192351
6 changed files with 412 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
/target
Cargo.lock
+20
View File
@@ -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"
+26
View File
@@ -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.
+84
View File
@@ -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
+66
View File
@@ -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
View File
@@ -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);
}
}