From 6a6819235155bc84fba146d94f2bcd055bb98dce Mon Sep 17 00:00:00 2001 From: Erik Hollensbe Date: Sun, 29 Aug 2021 16:25:28 -0700 Subject: [PATCH] hello, eggshell Signed-off-by: Erik Hollensbe --- .gitignore | 2 + Cargo.toml | 20 +++++ LICENSE | 26 ++++++ README.md | 84 ++++++++++++++++++ examples/basic.rs | 66 ++++++++++++++ src/lib.rs | 214 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 412 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/basic.rs create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4f8f830 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "eggshell" +version = "0.1.0" +edition = "2018" +authors = ["Erik Hollensbe ", "Adam Ierymenko "] +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" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c0825b0 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c89eff --- /dev/null +++ b/README.md @@ -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::(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::(None) + .await + .unwrap() + .len(); + + println!( + "before: {} containers, after: {} containers -- now dropping", + count, newcount + ); + + drop(gs); + + let newcount = docker + .lock() + .await + .list_containers::(None) + .await + .unwrap() + .len(); + + println!( + "after dropping: orig: {} containers, after: {} containers", + count, newcount + ); + + Ok(()) +} +``` + +## Author + +Erik Hollensbe + +## License + +BSD 3-Clause diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..1168bcb --- /dev/null +++ b/examples/basic.rs @@ -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::(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::(None) + .await + .unwrap() + .len(); + + println!( + "before: {} containers, after: {} containers -- now dropping", + count, newcount + ); + + drop(gs); + + let newcount = docker + .lock() + .await + .list_containers::(None) + .await + .unwrap() + .len(); + + println!( + "after dropping: orig: {} containers, after: {} containers", + count, newcount + ); + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8afb14f --- /dev/null +++ b/src/lib.rs @@ -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>, + containers: Vec, + 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>) -> Result { + 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, + start_options: Option>, + ) -> 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::(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::(None) + .await + .unwrap() + .len(); + + assert!(newcount == count + 10); + + drop(gs); + + let newcount = docker + .lock() + .await + .list_containers::(None) + .await + .unwrap() + .len(); + + assert!(newcount == count); + } +}