use std::collections::HashMap;
use bollard::query_parameters::{
DataUsageOptions, PruneBuildOptions, PruneContainersOptions, PruneImagesOptions,
PruneNetworksOptions, PruneVolumesOptions,
};
use super::{DockerClient, Resource, describe};
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct Usage {
pub total_size: i64,
pub reclaimable: i64,
pub count: i64,
pub active: i64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct DiskUsage {
pub images: Option<Usage>,
pub containers: Option<Usage>,
pub volumes: Option<Usage>,
pub build_cache: Option<Usage>,
}
impl DiskUsage {
fn parts(&self) -> impl Iterator<Item = &Usage> {
[
&self.images,
&self.containers,
&self.volumes,
&self.build_cache,
]
.into_iter()
.flatten()
}
pub fn total_size(&self) -> i64 {
self.parts().map(|u| u.total_size).sum()
}
pub fn reclaimable(&self) -> i64 {
self.parts().map(|u| u.reclaimable).sum()
}
pub fn is_reported(&self) -> bool {
self.parts().next().is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PruneKind {
StoppedContainers,
DanglingImages,
UnusedImages,
UnusedVolumes,
UnusedNetworks,
BuildCache,
}
impl PruneKind {
pub fn noun(self) -> &'static str {
match self {
PruneKind::StoppedContainers => "stopped containers",
PruneKind::DanglingImages => "dangling images",
PruneKind::UnusedImages => "unused images",
PruneKind::UnusedVolumes => "unused volumes",
PruneKind::UnusedNetworks => "unused networks",
PruneKind::BuildCache => "build cache",
}
}
pub fn affects(self) -> Resource {
match self {
PruneKind::StoppedContainers => Resource::Containers,
PruneKind::DanglingImages | PruneKind::UnusedImages => Resource::Images,
PruneKind::UnusedVolumes => Resource::Volumes,
PruneKind::UnusedNetworks => Resource::Networks,
PruneKind::BuildCache => Resource::Disk,
}
}
}
fn usage(
total_size: Option<i64>,
reclaimable: Option<i64>,
count: Option<i64>,
active: Option<i64>,
) -> Usage {
Usage {
total_size: total_size.unwrap_or_default(),
reclaimable: reclaimable.unwrap_or_default(),
count: count.unwrap_or_default(),
active: active.unwrap_or_default(),
}
}
impl DockerClient {
pub async fn disk_usage(&self) -> Result<DiskUsage, String> {
let df = self
.docker
.df(None::<DataUsageOptions>)
.await
.map_err(describe)?;
Ok(DiskUsage {
images: df
.image_usage
.map(|u| usage(u.total_size, u.reclaimable, u.total_count, u.active_count)),
containers: df
.container_usage
.map(|u| usage(u.total_size, u.reclaimable, u.total_count, u.active_count)),
volumes: df
.volume_usage
.map(|u| usage(u.total_size, u.reclaimable, u.total_count, u.active_count)),
build_cache: df
.build_cache_usage
.map(|u| usage(u.total_size, u.reclaimable, u.total_count, u.active_count)),
})
}
pub async fn prune(&self, kind: PruneKind) -> Result<i64, String> {
let docker = &self.docker;
let filter =
|key: &str, value: &str| HashMap::from([(key.to_owned(), vec![value.to_owned()])]);
let reclaimed = match kind {
PruneKind::StoppedContainers => docker
.prune_containers(None::<PruneContainersOptions>)
.await
.map(|r| r.space_reclaimed),
PruneKind::DanglingImages | PruneKind::UnusedImages => {
let dangling = if kind == PruneKind::DanglingImages {
"true"
} else {
"false"
};
let opts = PruneImagesOptions {
filters: Some(filter("dangling", dangling)),
};
docker
.prune_images(Some(opts))
.await
.map(|r| r.space_reclaimed)
}
PruneKind::UnusedVolumes => {
let opts = PruneVolumesOptions {
filters: Some(filter("all", "true")),
};
docker
.prune_volumes(Some(opts))
.await
.map(|r| r.space_reclaimed)
}
PruneKind::UnusedNetworks => docker
.prune_networks(None::<PruneNetworksOptions>)
.await
.map(|_| Some(0)),
PruneKind::BuildCache => {
let opts = PruneBuildOptions {
all: Some(true),
..Default::default()
};
docker
.prune_build(Some(opts))
.await
.map(|r| r.space_reclaimed)
}
};
reclaimed.map(Option::unwrap_or_default).map_err(describe)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn totals_skip_unreported_parts() {
let disk = DiskUsage {
images: Some(Usage {
total_size: 100,
reclaimable: 40,
..Default::default()
}),
build_cache: Some(Usage {
total_size: 50,
reclaimable: 50,
..Default::default()
}),
..Default::default()
};
assert_eq!(disk.total_size(), 150);
assert_eq!(disk.reclaimable(), 90);
assert!(disk.is_reported());
assert!(!DiskUsage::default().is_reported());
}
#[tokio::test]
#[ignore]
async fn live_daemon_reports_disk_usage() {
let client = DockerClient::connect().unwrap();
let disk = client.disk_usage().await.unwrap();
println!("{disk:?}");
assert!(disk.is_reported());
}
}