diff --git a/src/images.rs b/src/images.rs index 6a9bb42..ff8a0d6 100644 --- a/src/images.rs +++ b/src/images.rs @@ -1,141 +1,99 @@ -use chrono::NaiveDateTime; -use log::{error, warn}; -use serde::{self, Deserialize, Deserializer}; -use std::process::{exit, Command}; +use bollard::image::ListImagesOptions; +use bollard::models::ImageSummary; +use bollard::Docker; +use bollard::API_DEFAULT_VERSION; +use log::info; +use std::collections::HashMap; use crate::DateArgs; -use crate::DOCKER_BIN; const GHCR_REPO: &str = "ghcr.io/datahearth/clear-docker-images"; const DOCKER_REPO: &str = "datahearth/clear-docker-images"; -#[derive(Deserialize, Debug)] -struct Image { - // image ID - #[serde(rename = "ID")] - id: String, - // image repository - #[serde(rename = "Repository")] - repository: String, - // image tag - #[serde(rename = "Tag")] - tag: String, - // image creation date as UNIX timestamp - #[serde(deserialize_with = "deserialize_creation_date", rename = "CreatedAt")] - created_at: i64, - // image size in MB - #[serde(deserialize_with = "deserialize_size", rename = "Size")] - size: f32, -} - -pub fn deserialize_creation_date<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let date = String::deserialize(deserializer)?; - - // format => 2021-01-01 00:00:00 +0100 CET - NaiveDateTime::parse_from_str(&date, "%Y-%m-%d %H:%M:%S %z %Z") - .map(|d| d.timestamp()) - .map_err(serde::de::Error::custom) -} - -pub fn deserialize_size<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let size = String::deserialize(deserializer)?; - - if size.contains("KB") { - size.replace("KB", "") - .parse::() - .map(|s| s / 1000.0) - .map_err(serde::de::Error::custom) - } else if size.contains("MB") { - size.replace("MB", "") - .parse::() - .map_err(serde::de::Error::custom) - } else if size.contains("GB") { - size.replace("GB", "") - .parse::() - .map(|s| s * 1000.0) - .map_err(serde::de::Error::custom) - } else { - Err(serde::de::Error::custom(format!( - "Unknown size identification: {}", - size, - ))) - } -} - -pub fn process_imgs( +pub struct DockerActions { + docker: Docker, repository: Option, tags: Vec, - timestamps: DateArgs, -) -> (Vec, f32) { - let mut ids = vec![]; - let mut saved_size = 0.0; + date: DateArgs, +} - for img in parse_imgs(repository) { - let image: Image = serde_json::from_str(&img).unwrap(); - let del = timestamps - .stop - .map_or(timestamps.start > image.created_at, |stop| { - timestamps.start > image.created_at && image.created_at > stop - }); - - if del && (image.repository != GHCR_REPO && image.repository != DOCKER_REPO) { - if !tags.contains(&image.tag) { - ids.push(image.id); - - saved_size += image.size - } +impl DockerActions { + pub fn new( + socket: String, + repository: Option, + tags: Vec, + date: DateArgs, + ) -> Self { + Self { + docker: Docker::connect_with_socket(&socket, 120, API_DEFAULT_VERSION).unwrap(), + repository, + tags, + date, } } - return (ids, saved_size); -} + pub async fn get(&self) -> Result, bollard::errors::Error> { + let mut image_filters = HashMap::new(); -fn get_images(repo: Option) -> Vec { - let mut cmd = Command::new(DOCKER_BIN); - cmd.arg("images"); + // why using &self.repository instead of selft.repository ? + if let Some(r) = &self.repository { + image_filters.insert("reference", vec![r.as_str()]); + } - repo.map(|repo| cmd.arg(repo)); + self.docker + .list_images(Some(ListImagesOptions { + all: true, + filters: image_filters, + ..Default::default() + })) + .await + } - cmd.args(["--format", "{{json .}}"]); + pub async fn delete( + &self, + images: Vec, + dry_run: bool, + ) -> Result { + let mut removed_size = 0; + for image in images { + info!("deleting: {}", image.id); - match cmd.output() { - Ok(o) => { - if !o.status.success() { - error!( - "{}", - std::str::from_utf8(&o.stderr).expect("failed to parse STDERR to UTF-8") - ); - error!("failed to retrieve docker images. Please checkout STDERR"); - exit(1); + if !dry_run { + if let Err(e) = self.docker.delete_service(&image.id).await { + return Err(e); + } } - o.stdout + removed_size += image.size; } - Err(e) => { - error!("docker command failed: {}", e); - exit(1); - } - } -} -fn parse_imgs(repository: Option) -> Vec { - let stdout = get_images(repository); - - let output = String::from_utf8(stdout).unwrap_or_else(|e| { - error!("failed to parse docker output: {}", e); - exit(1); - }); - let images: Vec = output.lines().map(|s| s.to_string()).collect(); - - if images.len() == 0 { - warn!("No images found for current timestamp and/or repository"); + Ok(removed_size) } - return images; + pub fn filter(&self, images: Vec) -> Vec { + let mut to_be_deleted: Vec = vec![]; + + for image in images { + if self + .date + .stop + .map_or(self.date.start > image.created, |stop| { + self.date.start > image.created && image.created > stop + }) + && image.repo_tags.iter().any(|tag| { + !tag.contains(GHCR_REPO) + && !tag.contains(DOCKER_REPO) + && self + .tags + .iter() + .any(|excluded_tag| !tag.contains(excluded_tag)) + }) + { + println!("{:?}", self.tags); + to_be_deleted.push(image); + } + } + + return to_be_deleted; + } } diff --git a/src/main.rs b/src/main.rs index fdd0f08..dbd14ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,11 +4,10 @@ use chrono::{NaiveDateTime, Utc}; use clap::Parser; use log::{error, info}; use simple_logger::SimpleLogger; -use std::process::{exit, Command, Stdio}; +use std::process::exit; -use crate::images::process_imgs; +use crate::images::DockerActions; -const DOCKER_BIN: &str = "docker"; const TWO_DAYS_TIMESTAMP: i64 = 172_800; /// Clear docker images from @@ -32,13 +31,13 @@ struct Args { #[clap(long, takes_value = false)] dry_run: bool, - /// force image removal [default: false] - #[clap(long, takes_value = false)] - force: bool, - /// add more logs [default: false] #[clap(short, long, takes_value = false)] verbose: bool, + + /// where is located the docker socket (can be a UNIX socket or TCP protocol) + #[clap(short, long, default_value = "/var/run/docker.sock")] + socket: String, } #[derive(Debug)] @@ -47,7 +46,8 @@ pub struct DateArgs { stop: Option, } -fn main() { +#[tokio::main] +async fn main() { let args = Args::parse(); let logger = SimpleLogger::new() .without_timestamps() @@ -61,9 +61,10 @@ fn main() { exit(1); } - let (ids, saved_size) = process_imgs( + let actions = DockerActions::new( + args.socket, args.repository, - args.tags.map_or(vec![], |tags| tags), + args.tags.map_or(vec![], |t| t), args.date.map_or( DateArgs { start: Utc::now().timestamp() - TWO_DAYS_TIMESTAMP, @@ -73,47 +74,28 @@ fn main() { ), ); - if args.dry_run { - info!("dry run activated"); - } else { - let mut cmd = Command::new(DOCKER_BIN); - cmd.arg("rmi"); - - if args.force { - info!("\"--force\" flag set"); - cmd.arg("--force"); + let images = match actions.get().await { + Ok(i) => i, + Err(e) => { + error!("failed to retrieve docker images: {}", e); + exit(1); } + }; - if ids.len() == 0 { - info!("nothing to do..."); - return; + let saved = match actions.delete(actions.filter(images), args.dry_run).await { + Ok(s) => s, + Err(e) => { + error!("failed to retrieve docker images: {}", e); + exit(1); } + }; - if args.verbose { - info!("trigger \"docker rmi\" command"); - } - - match cmd.args(&ids).stdout(Stdio::null()).status() { - Ok(s) => { - if !s.success() { - error!("failed to delete images. Please checkout STDERR") - } - - info!("images deleted!") - } - Err(e) => error!("docker command failed: {}", e), - }; - } - - if args.dry_run { - info!("deleted images: {:#?}", ids); - } info!( "Total disk space saved: {}", - if saved_size / 1000.0 > 1.0 { - format!("{:.2}GB", saved_size / 1000.0) + if saved / 1000_000 >= 1000 { + format!("{:.2}GB", saved as f64 / 1000_000_000.0) } else { - format!("{:.2}MB", saved_size) + format!("{:.2}MB", saved as f32 / 1000_000.0) } ); }