refactor code => dropping docker cli
This commit is contained in:
parent
60d0c8a17e
commit
c38afccd63
194
src/images.rs
194
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<i64, D::Error>
|
||||
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<f32, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let size = String::deserialize(deserializer)?;
|
||||
|
||||
if size.contains("KB") {
|
||||
size.replace("KB", "")
|
||||
.parse::<f32>()
|
||||
.map(|s| s / 1000.0)
|
||||
.map_err(serde::de::Error::custom)
|
||||
} else if size.contains("MB") {
|
||||
size.replace("MB", "")
|
||||
.parse::<f32>()
|
||||
.map_err(serde::de::Error::custom)
|
||||
} else if size.contains("GB") {
|
||||
size.replace("GB", "")
|
||||
.parse::<f32>()
|
||||
.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<String>,
|
||||
tags: Vec<String>,
|
||||
timestamps: DateArgs,
|
||||
) -> (Vec<String>, 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<String>,
|
||||
tags: Vec<String>,
|
||||
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<Vec<ImageSummary>, bollard::errors::Error> {
|
||||
let mut image_filters = HashMap::new();
|
||||
|
||||
fn get_images(repo: Option<String>) -> Vec<u8> {
|
||||
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<ImageSummary>,
|
||||
dry_run: bool,
|
||||
) -> Result<i64, bollard::errors::Error> {
|
||||
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<String>) -> Vec<String> {
|
||||
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<String> = 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<ImageSummary>) -> Vec<ImageSummary> {
|
||||
let mut to_be_deleted: Vec<ImageSummary> = 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;
|
||||
}
|
||||
}
|
||||
|
|
70
src/main.rs
70
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<i64>,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
Reference in New Issue