From f613efc8829f724d6bd50379839c160c5832fa71 Mon Sep 17 00:00:00 2001 From: aleidk Date: Thu, 26 Jun 2025 11:52:49 -0400 Subject: [PATCH] refactor: introduce local library strucs for containers --- Cargo.lock | 39 ++++++++++++++++++++++++ Cargo.toml | 2 ++ src/docker.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++ src/docker/labels.rs | 0 src/lib.rs | 1 + src/manager.rs | 63 ++++++++++++++------------------------ 6 files changed, 136 insertions(+), 41 deletions(-) create mode 100644 src/docker.rs create mode 100644 src/docker/labels.rs diff --git a/Cargo.lock b/Cargo.lock index 2a50a5e..27760b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + [[package]] name = "autocfg" version = "1.5.0" @@ -183,6 +189,8 @@ version = "0.1.0" dependencies = [ "bollard", "futures", + "serde", + "serde_yml", "thiserror", "tokio", ] @@ -613,6 +621,16 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] + [[package]] name = "litemap" version = "0.8.0" @@ -855,6 +873,21 @@ dependencies = [ "time", ] +[[package]] +name = "serde_yml" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" +dependencies = [ + "indexmap 2.9.0", + "itoa", + "libyml", + "memchr", + "ryu", + "serde", + "version_check", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1066,6 +1099,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 3bc80a3..31a1032 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,7 @@ edition = "2024" [dependencies] bollard = "0.19.1" futures = "0.3.31" +serde = "1.0.219" +serde_yml = "0.0.12" thiserror = "2.0.12" tokio = { version = "1.45.1", features = ["macros", "rt", "rt-multi-thread"] } diff --git a/src/docker.rs b/src/docker.rs new file mode 100644 index 0000000..0f3c031 --- /dev/null +++ b/src/docker.rs @@ -0,0 +1,72 @@ +use bollard::models::{ContainerSummary, ContainerSummaryStateEnum, MountPoint}; +use serde::de::DeserializeOwned; +use serde_yml::Value; +use std::collections::HashMap; + +mod labels; + +#[derive(Debug)] +pub struct Container { + pub id: String, + pub name: String, + pub image: String, + pub state: ContainerSummaryStateEnum, + pub mounts: Vec, + pub labels: Labels, +} + +impl From for Container { + fn from(value: ContainerSummary) -> Self { + let container_name: String = value + .names + .unwrap_or_default() + .iter() + .map(|name| name.strip_prefix('/').unwrap_or(name).to_string()) + .collect(); + + Self { + id: value.id.unwrap_or_default(), + name: container_name, + image: value.image.unwrap_or_default(), + state: value.state.unwrap_or(ContainerSummaryStateEnum::EMPTY), + mounts: value.mounts.unwrap_or_default(), + labels: value.labels.unwrap_or_default().into(), + } + } +} + +#[derive(Debug)] +pub struct Labels { + pub enable: bool, + pub service: ServiceConfig, +} + +#[derive(Debug)] +pub struct ServiceConfig { + pub compose_hash: Option, + pub group: Option, +} + +impl Labels { + fn parse_label(value: Option<&String>) -> T { + if value.is_none() { + return T::default(); + } + + let v: Value = serde_yml::from_str(value.unwrap()).unwrap_or_default(); + + serde_yml::from_value(v).unwrap_or_default() + } +} + +impl From> for Labels { + fn from(value: HashMap) -> Self { + Self { + enable: Self::parse_label(value.get("epoch.enable")), + service: ServiceConfig { + compose_hash: Self::parse_label(value.get("com.docker.compose.config-hash")), + group: Self::parse_label(value.get("epoch.service.group")), + }, + } + } +} diff --git a/src/docker/labels.rs b/src/docker/labels.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib.rs b/src/lib.rs index aeb5467..b721eb6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod error; pub mod manager; +mod docker; pub use error::{Error, Result}; diff --git a/src/manager.rs b/src/manager.rs index 086dab3..3c2f7fe 100644 --- a/src/manager.rs +++ b/src/manager.rs @@ -1,6 +1,6 @@ use crate::Error; -use bollard::Docker; use bollard::models::ContainerSummary; +use bollard::Docker; use std::collections::HashMap; /// Namespace to manage containers together (like compose projects) @@ -9,42 +9,33 @@ 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 { let mut services = HashMap::new(); - for container in value.iter().cloned() { - if container - .mounts - .as_deref() - .is_none_or(|mounts| mounts.is_empty()) - { + for summary in value.iter().cloned() { + let container = crate::docker::Container::from(summary); + + if container.mounts.is_empty() { continue; } - let project = match &container.labels { - None => DEFAULT_GROUP.to_owned(), - Some(labels) => { - // Returns user provided group if exits - if labels.contains_key("epoch.service.group") { - labels - .get("epoch.service.group") - .and_then(|value| Some(value.to_owned())) - .unwrap() - } else { - labels - // Group by compose hash - .get("com.docker.compose.config-hash") - .and_then(|value| Some(value.to_owned())) - // No group found - .unwrap_or(DEFAULT_GROUP.to_owned()) - } - } + let group = match &container.labels.service.group { + // Doesn't have a group defined by the user, + // try to use the compose hash or throw it with the default group + None => container + .labels + .service + .compose_hash + .as_ref() + .map(|s| s.to_string()) + .unwrap_or_else(|| DEFAULT_GROUP.to_owned()), + Some(group) => group.to_owned(), }; - let list: &mut Vec = services.entry(project).or_default(); + let list: &mut Vec = services.entry(group).or_default(); list.push(container); } @@ -62,16 +53,11 @@ pub async fn manage(containers: &Vec) -> crate::Result<()> { for (group, containers) in services.0.iter() { // stop containers of service group let stop_tasks = containers.into_iter().map(async |container| { - let id = container - .id - .as_ref() - .ok_or(Error::Static("Error getting containers id"))?; - - eprintln!("Stoping container: {:#?}", id); + eprintln!("Stoping container: {:#?}", container.id); let stop_opts = bollard::query_parameters::StopContainerOptionsBuilder::new().build(); - docker.stop_container(id, Some(stop_opts)).await?; + docker.stop_container(&container.id, Some(stop_opts)).await?; Ok::<(), crate::Error>(()) }); @@ -84,16 +70,11 @@ pub async fn manage(containers: &Vec) -> crate::Result<()> { // restart the containers let start_tasks = containers.into_iter().map(async |container| { - let id = container - .id - .as_ref() - .ok_or(Error::Static("Error getting containers id"))?; - - eprintln!("Starting container: {:#?}", id); + eprintln!("Starting container: {:#?}", container.id); let start_opts = bollard::query_parameters::StartContainerOptionsBuilder::new().build(); - docker.start_container(id, Some(start_opts)).await?; + docker.start_container(&container.id, Some(start_opts)).await?; Ok::<(), crate::Error>(()) });