From d0afb2955be62a5415e5fdf6d6e327161511a543 Mon Sep 17 00:00:00 2001 From: aleidk Date: Thu, 26 Jun 2025 17:00:23 -0400 Subject: [PATCH] feat: add run_backup_container skeleton --- Cargo.lock | 122 ++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- src/docker.rs | 72 +++++++++++++++++++++++++++-- src/main.rs | 14 +++++- src/manager.rs | 115 +++++++++++++++++++++++++++++++++++++--------- 5 files changed, 299 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b98ddcd..4185750 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -65,6 +74,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + [[package]] name = "bollard" version = "0.19.1" @@ -195,6 +210,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -307,6 +323,18 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + [[package]] name = "gimli" version = "0.31.1" @@ -651,6 +679,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.7.5" @@ -673,7 +710,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -774,6 +811,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "ref-cast" version = "1.0.24" @@ -794,6 +837,50 @@ dependencies = [ "syn", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustc-demangle" version = "0.1.25" @@ -1142,10 +1229,14 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -1179,6 +1270,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -1206,6 +1308,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -1427,6 +1538,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + [[package]] name = "writeable" version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index 3b253ad..b77a3ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,5 @@ serde_yml = "0.0.12" thiserror = "2.0.12" tokio = { version = "1.45.1", features = ["macros", "rt", "rt-multi-thread"] } tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing-subscriber = { version = "0.3.19", features = ["env-filter", "std"] } +uuid = { version = "1.17.0", features = ["v4"] } diff --git a/src/docker.rs b/src/docker.rs index 303bc99..490f012 100644 --- a/src/docker.rs +++ b/src/docker.rs @@ -1,8 +1,8 @@ use bollard::models::{ContainerSummary, ContainerSummaryStateEnum, MountPoint}; +use bollard::secret::ContainerCreateBody; use serde::de::DeserializeOwned; use serde_yml::Value; use std::collections::HashMap; -use tracing::field::{Field, Visit}; mod labels; @@ -16,6 +16,20 @@ pub struct Container { pub labels: Labels, } +impl Default for Container { + fn default() -> Self { + Container { + id: String::default(), + name: String::default(), + image: String::default(), + state: ContainerSummaryStateEnum::EMPTY, + + mounts: vec![], + labels: Default::default(), + } + } +} + impl From for Container { fn from(value: ContainerSummary) -> Self { let container_name: String = value @@ -36,13 +50,65 @@ impl From for Container { } } -#[derive(Debug)] +impl From<&Container> for ContainerCreateBody { + fn from(value: &Container) -> Self { + ContainerCreateBody { + image: Some(value.image.clone()), + labels: Some(HashMap::from([ + (String::from("epoch.internal.managed"), String::from("true")), + ( + String::from("epoch.internal.ephemeral"), + String::from("true"), + ), + ])), + ..Default::default() + } + } +} + +pub struct ContainerBuilder { + container: Container +} + +impl ContainerBuilder { + pub fn new() -> Self { + Self { + container: Container::default() + } + } + + pub fn build(self) -> Container { + self.container + } + + pub fn with_id(mut self, id: impl Into) -> Self { + self.container.id = id.into(); + self + } + + pub fn with_image(mut self, image: impl Into) -> Self { + self.container.image = image.into(); + self + } + + pub fn with_mounts(mut self, mounts: Vec) -> Self { + self.container.mounts = mounts; + self + } + + pub fn with_labels(mut self, labels: HashMap) -> Self { + self.container.labels = labels.into(); + self + } +} + +#[derive(Debug, Default)] pub struct Labels { pub enable: bool, pub service: ServiceConfig, } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct ServiceConfig { pub compose_hash: Option, pub group: Option, diff --git a/src/main.rs b/src/main.rs index 8840bc7..e32485c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,34 @@ use bollard::Docker; use std::collections::HashMap; use tokio; +use tracing::info; +use tracing_subscriber::EnvFilter; #[tokio::main] async fn main() -> epoch::Result<()> { // construct a subscriber that prints formatted traces to stdout - tracing_subscriber::fmt().with_ansi(true).compact().init(); + tracing_subscriber::fmt() + .with_ansi(true) + .with_line_number(true) + .with_env_filter(EnvFilter::from_default_env()) + .init(); let docker = Docker::connect_with_local_defaults()?; let filters: HashMap<&str, Vec<&str>> = HashMap::from([("label", vec!["epoch.manage=true"])]); let opts = bollard::query_parameters::ListContainersOptionsBuilder::new() + .all(true) .filters(&filters) .build(); let containers = docker.list_containers(Some(opts)).await?; + if containers.is_empty() { + info!("No containers found, exiting"); + return Ok(()); + } + epoch::manager::manage(&containers).await?; Ok(()) diff --git a/src/manager.rs b/src/manager.rs index 98d39a5..66da8d2 100644 --- a/src/manager.rs +++ b/src/manager.rs @@ -1,10 +1,11 @@ -use crate::docker::Container; +use crate::docker::{Container, ContainerBuilder}; use crate::Error; use bollard::models::ContainerSummary; use bollard::Docker; use futures::future::try_join_all; +use futures::StreamExt; use std::collections::HashMap; -use tracing::info; +use tracing::{debug, info, instrument}; /// Namespace to manage containers together (like compose projects) type ServiceGroup = String; @@ -12,7 +13,7 @@ type ServiceGroup = String; const DEFAULT_GROUP: &str = "NONE"; #[derive(Debug)] -struct Services(HashMap>); +struct Services(HashMap>); impl From<&Vec> for Services { fn from(value: &Vec) -> Self { @@ -38,7 +39,7 @@ impl From<&Vec> for Services { Some(group) => group.to_owned(), }; - let list: &mut Vec = services.entry(group).or_default(); + let list: &mut Vec = services.entry(group).or_default(); list.push(container); } @@ -48,43 +49,115 @@ impl From<&Vec> for Services { #[tracing::instrument(skip(containers))] pub async fn manage(containers: &Vec) -> crate::Result<()> { + let total_containers = containers.len(); let services = Services::from(containers); + info!( + "Found {} containers, grouped into {} groups", + total_containers, + services.0.len() + ); + // TODO: reuse main instance let docker = Docker::connect_with_local_defaults()?; // TODO: iterate over groups in parallel for (group, containers) in services.0.iter() { - // stop containers of service group - try_join_all(containers.into_iter().map(|container | stop_container(&docker, container))).await?; - - // create container with the same mounts as each container in the group - - // run the new container - - // restart the containers - try_join_all(containers.into_iter().map(|container| start_container(&docker, container))).await?; + handle_group(&docker, group, containers).await?; } + // Do cleanup to ensure all containers that are created are also deleted + Ok(()) } -async fn stop_container(docker: &Docker, container: &crate::docker::Container) -> crate::Result<()> { - info!("Stoping container: {:#?}", container.id); +#[tracing::instrument(skip(docker, containers))] +async fn handle_group( + docker: &Docker, + _group: &ServiceGroup, + containers: &Vec, +) -> Result<(), Error> { + // stop containers of service group + try_join_all( + containers + .into_iter() + .map(|container| stop_container(&docker, &container.id)), + ) + .await?; + + // create container with the same mounts as each container in the group + let backup_container = ContainerBuilder::new().with_image("hello-world").build(); + + run_backup_container(&docker, &backup_container).await?; + + // run the new container + + // restart the containers + try_join_all( + containers + .into_iter() + .map(|container| start_container(&docker, &container.id)), + ) + .await?; + Ok(()) +} + +async fn stop_container( + docker: &Docker, + container_id: impl AsRef, +) -> crate::Result<()> { + let container_id = container_id.as_ref(); + info!("Stoping container: {}", container_id); let stop_opts = bollard::query_parameters::StopContainerOptionsBuilder::new().build(); - docker.stop_container(&container.id, Some(stop_opts)).await?; + docker + .stop_container(container_id, Some(stop_opts)) + .await?; - Ok::<(), crate::Error>(()) + Ok(()) } -async fn start_container(docker: &Docker, container: &crate::docker::Container) -> crate::Result<()> { - info!("Starting container: {:#?}", container.id); +async fn start_container( + docker: &Docker, + container_id: impl AsRef, +) -> crate::Result<()> { + let container_id = container_id.as_ref(); + info!("Starting container: {}", container_id); let start_opts = bollard::query_parameters::StartContainerOptionsBuilder::new().build(); - docker.start_container(&container.id, Some(start_opts)).await?; + docker + .start_container(container_id, Some(start_opts)) + .await?; - Ok::<(), crate::Error>(()) + Ok(()) +} + +#[instrument(skip_all, fields(container_id))] +async fn run_backup_container(docker: &Docker, container: &Container) -> crate::Result<()> { + info!("Creating container from image {:?}",&container.image); + debug!("{:#?}", &container); + + let create_opts = bollard::query_parameters::CreateContainerOptionsBuilder::default().build(); + let create_conf = bollard::models::ContainerCreateBody::from(container); + + let response = docker + .create_container(Some(create_opts), create_conf) + .await?; + + tracing::Span::current().record("container_id", &response.id); + + start_container(docker, &response.id).await?; + + let wait_opts = bollard::query_parameters::WaitContainerOptionsBuilder::default().build(); + let mut stream = docker.wait_container(&response.id, Some(wait_opts)); + + while let Some(status) = stream.next().await { + debug!("Received status: {:?}", status?); + } + + stop_container(docker, &response.id).await?; + + Ok(()) }