feat: add run_backup_container skeleton

This commit is contained in:
Alexander Navarro 2025-06-26 17:00:23 -04:00
parent 1f194ca7f1
commit d0afb2955b
5 changed files with 299 additions and 27 deletions

122
Cargo.lock generated
View file

@ -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"

View file

@ -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"] }

View file

@ -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<ContainerSummary> for Container {
fn from(value: ContainerSummary) -> Self {
let container_name: String = value
@ -36,13 +50,65 @@ impl From<ContainerSummary> 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<String>) -> Self {
self.container.id = id.into();
self
}
pub fn with_image(mut self, image: impl Into<String>) -> Self {
self.container.image = image.into();
self
}
pub fn with_mounts(mut self, mounts: Vec<MountPoint>) -> Self {
self.container.mounts = mounts;
self
}
pub fn with_labels(mut self, labels: HashMap<String, String>) -> 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<String>,
pub group: Option<String>,

View file

@ -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(())

View file

@ -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<ServiceGroup, Vec<crate::docker::Container>>);
struct Services(HashMap<ServiceGroup, Vec<Container>>);
impl From<&Vec<ContainerSummary>> for Services {
fn from(value: &Vec<ContainerSummary>) -> Self {
@ -38,7 +39,7 @@ impl From<&Vec<ContainerSummary>> for Services {
Some(group) => group.to_owned(),
};
let list: &mut Vec<crate::docker::Container> = services.entry(group).or_default();
let list: &mut Vec<Container> = services.entry(group).or_default();
list.push(container);
}
@ -48,43 +49,115 @@ impl From<&Vec<ContainerSummary>> for Services {
#[tracing::instrument(skip(containers))]
pub async fn manage(containers: &Vec<ContainerSummary>) -> 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<Container>,
) -> 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<str>,
) -> 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<str>,
) -> 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(())
}