refactor: introduce local library strucs for containers

This commit is contained in:
Alexander Navarro 2025-06-26 11:52:49 -04:00
parent 8e89aeba75
commit f613efc882
6 changed files with 136 additions and 41 deletions

39
Cargo.lock generated
View file

@ -32,6 +32,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@ -183,6 +189,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bollard", "bollard",
"futures", "futures",
"serde",
"serde_yml",
"thiserror", "thiserror",
"tokio", "tokio",
] ]
@ -613,6 +621,16 @@ version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 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]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.0" version = "0.8.0"
@ -855,6 +873,21 @@ dependencies = [
"time", "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]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@ -1066,6 +1099,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"

View file

@ -6,5 +6,7 @@ edition = "2024"
[dependencies] [dependencies]
bollard = "0.19.1" bollard = "0.19.1"
futures = "0.3.31" futures = "0.3.31"
serde = "1.0.219"
serde_yml = "0.0.12"
thiserror = "2.0.12" thiserror = "2.0.12"
tokio = { version = "1.45.1", features = ["macros", "rt", "rt-multi-thread"] } tokio = { version = "1.45.1", features = ["macros", "rt", "rt-multi-thread"] }

72
src/docker.rs Normal file
View file

@ -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<MountPoint>,
pub labels: Labels,
}
impl From<ContainerSummary> 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<String>,
pub group: Option<String>,
}
impl Labels {
fn parse_label<T: Default + DeserializeOwned>(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<HashMap<String, String>> for Labels {
fn from(value: HashMap<String, String>) -> 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")),
},
}
}
}

0
src/docker/labels.rs Normal file
View file

View file

@ -1,4 +1,5 @@
pub mod error; pub mod error;
pub mod manager; pub mod manager;
mod docker;
pub use error::{Error, Result}; pub use error::{Error, Result};

View file

@ -1,6 +1,6 @@
use crate::Error; use crate::Error;
use bollard::Docker;
use bollard::models::ContainerSummary; use bollard::models::ContainerSummary;
use bollard::Docker;
use std::collections::HashMap; use std::collections::HashMap;
/// Namespace to manage containers together (like compose projects) /// Namespace to manage containers together (like compose projects)
@ -9,42 +9,33 @@ type ServiceGroup = String;
const DEFAULT_GROUP: &str = "NONE"; const DEFAULT_GROUP: &str = "NONE";
#[derive(Debug)] #[derive(Debug)]
struct Services(HashMap<ServiceGroup, Vec<ContainerSummary>>); struct Services(HashMap<ServiceGroup, Vec<crate::docker::Container>>);
impl From<&Vec<ContainerSummary>> for Services { impl From<&Vec<ContainerSummary>> for Services {
fn from(value: &Vec<ContainerSummary>) -> Self { fn from(value: &Vec<ContainerSummary>) -> Self {
let mut services = HashMap::new(); let mut services = HashMap::new();
for container in value.iter().cloned() { for summary in value.iter().cloned() {
if container let container = crate::docker::Container::from(summary);
.mounts
.as_deref() if container.mounts.is_empty() {
.is_none_or(|mounts| mounts.is_empty())
{
continue; continue;
} }
let project = match &container.labels { let group = match &container.labels.service.group {
None => DEFAULT_GROUP.to_owned(), // Doesn't have a group defined by the user,
Some(labels) => { // try to use the compose hash or throw it with the default group
// Returns user provided group if exits None => container
if labels.contains_key("epoch.service.group") { .labels
labels .service
.get("epoch.service.group") .compose_hash
.and_then(|value| Some(value.to_owned())) .as_ref()
.unwrap() .map(|s| s.to_string())
} else { .unwrap_or_else(|| DEFAULT_GROUP.to_owned()),
labels Some(group) => group.to_owned(),
// 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 list: &mut Vec<ContainerSummary> = services.entry(project).or_default(); let list: &mut Vec<crate::docker::Container> = services.entry(group).or_default();
list.push(container); list.push(container);
} }
@ -62,16 +53,11 @@ pub async fn manage(containers: &Vec<ContainerSummary>) -> crate::Result<()> {
for (group, containers) in services.0.iter() { for (group, containers) in services.0.iter() {
// stop containers of service group // stop containers of service group
let stop_tasks = containers.into_iter().map(async |container| { let stop_tasks = containers.into_iter().map(async |container| {
let id = container eprintln!("Stoping container: {:#?}", container.id);
.id
.as_ref()
.ok_or(Error::Static("Error getting containers id"))?;
eprintln!("Stoping container: {:#?}", id);
let stop_opts = bollard::query_parameters::StopContainerOptionsBuilder::new().build(); 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>(()) Ok::<(), crate::Error>(())
}); });
@ -84,16 +70,11 @@ pub async fn manage(containers: &Vec<ContainerSummary>) -> crate::Result<()> {
// restart the containers // restart the containers
let start_tasks = containers.into_iter().map(async |container| { let start_tasks = containers.into_iter().map(async |container| {
let id = container eprintln!("Starting container: {:#?}", container.id);
.id
.as_ref()
.ok_or(Error::Static("Error getting containers id"))?;
eprintln!("Starting container: {:#?}", id);
let start_opts = bollard::query_parameters::StartContainerOptionsBuilder::new().build(); 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>(()) Ok::<(), crate::Error>(())
}); });