diff --git a/Cargo.lock b/Cargo.lock index 738719e..bcd2f40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,6 +98,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi 0.3.9", +] + [[package]] name = "core-foundation" version = "0.7.0" @@ -278,7 +291,7 @@ checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" dependencies = [ "cfg-if 0.1.10", "libc", - "wasi", + "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] @@ -674,6 +687,25 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -887,6 +919,7 @@ name = "registry-cleaner" version = "0.1.0" dependencies = [ "args", + "chrono", "getopts", "reqwest", "serde", @@ -1165,6 +1198,17 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi 0.3.9", +] + [[package]] name = "tinyvec" version = "0.3.4" @@ -1415,6 +1459,12 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasm-bindgen" version = "0.2.68" diff --git a/Cargo.toml b/Cargo.toml index ba16ea9..0686915 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" [dependencies] args = "2.2.0" +chrono = "0.4.19" getopts = "0.2.21" reqwest = {version = "0.10.8", features =["json", "rustls-tls", "trust-dns", "blocking"]} serde = {version = "1.0.117", features = ["derive"] } diff --git a/src/api/registry.rs b/src/api/registry.rs index b75d8bf..944b3e3 100644 --- a/src/api/registry.rs +++ b/src/api/registry.rs @@ -1,5 +1,6 @@ use reqwest; use serde::Deserialize; +use std::collections::HashMap; use url::Url; pub struct Registry<'a> { @@ -15,10 +16,28 @@ pub struct RepositoryList { #[allow(dead_code)] #[derive(Debug, Deserialize)] pub struct RepositoryTags { - name: String, - tags: Option>, + pub name: String, + pub tags: Option>, } +#[allow(dead_code, non_snake_case)] +#[derive(Debug, Deserialize)] +pub struct RepositoryTagManifest { + pub name: String, + pub tag: String, + pub history: Vec>, + #[serde(skip)] + pub ts: i64, + #[serde(skip)] + pub digest: String, +} + +#[derive(Debug, Deserialize)] +struct History { + created: Option, +} + + impl<'a> Registry<'a> { pub async fn get_repositories(&self) -> Result> { let registry_url = Url::parse(self.url).unwrap(); @@ -43,9 +62,100 @@ impl<'a> Registry<'a> { let resp = reqwest::get(api_url).await?; - println!("{:?}", resp.headers()); - let data = resp.json::().await?; Ok(data) } + + pub async fn get_repository_tag_manifest( + &self, + repo: &str, + tag: &str, + ) -> Result> { + let registry_url = Url::parse(self.url).unwrap(); + let api_url = registry_url + .join(format!("/v2/{}/manifests/{}", repo, tag).as_str()) + .unwrap(); + + let resp = reqwest::get(api_url.clone()).await?; + + // let digest = resp + // .headers() + // .get("Docker-Content-Digest") + // .unwrap() + // .to_str() + // .unwrap() + // .to_string(); + + let digest = reqwest::Client::new() + .get(api_url.clone()) + .header( + reqwest::header::ACCEPT, + "application/vnd.docker.distribution.manifest.v2+json", + ) + .send() + .await? + .headers() + .get("Docker-Content-Digest") + .unwrap() + .to_str() + .unwrap() + .to_string(); + + let mut data = resp.json::().await?; + + let ts = data.history.iter().fold(0, |acc, d| { + match d.get("v1Compatibility") { + None => acc, + Some(s) => { + // println!("{}", (*s)) + let h: History = serde_json::from_str(s).unwrap_or(History { created: None }); + + match h.created { + None => acc, + Some(time_str) => { + let result = chrono::DateTime::parse_from_rfc3339(time_str.as_str()); + match result { + Ok(t) => { + let tt = t.timestamp(); + if acc > tt { + acc + } else { + tt + } + } + Err(_) => acc, + } + } + } + } + } + }); + + data.history.clear(); + data.ts = ts; + data.digest = digest; + + Ok(data) + } + pub async fn delete_repository_tag( + &self, + repo: &str, + digest: &str, + ) -> Result<(), Box> { + let registry_url = Url::parse(self.url).unwrap(); + let api_url = registry_url + .join(format!("/v2/{}/manifests/{}", repo, digest).as_str()) + .unwrap(); + // println!("url:: {}", api_url); + + let client = reqwest::Client::new(); + client.delete(api_url).send().await?; + + // println!("Status : {}", resp.status()); + // if resp.status().as_u16() >= 400 { + + // } + + Ok(()) + } } diff --git a/src/main.rs b/src/main.rs index 86c17c2..85d5779 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ struct Opts { registry: String, image: String, exclude: Vec, + keep: i32, } fn main() { @@ -28,22 +29,55 @@ fn main() { if res.repositories.len() == 0 { return; } - println!("response :::: {:?}", res); + // println!("response :::: {:?}", res); let mut proc_images: Vec = Vec::new(); if _parsed.image.len() > 0 { + if !res.repositories.contains(&_parsed.image) { + eprintln!("image not in registry"); + std::process::exit(1); + } proc_images.push(_parsed.image); } else { proc_images = res.repositories.to_owned(); } - println!("proc images ::: {:?}", proc_images); + // println!("proc images ::: {:?}", proc_images); for img in proc_images.into_iter() { let _repo_tags = registry.get_repository_tags(img.as_str()).await.unwrap(); - println!("{:?}", _repo_tags); + let mut tags: Vec = Vec::new(); + + match _repo_tags.tags { + Some(s) => { + for x in s.iter() { + if _parsed.exclude.contains(x) { + continue; + } + let manifests = registry + .get_repository_tag_manifest(img.as_str(), x) + .await + .unwrap(); + tags.push(manifests); + } + } + _ => (), + } + + tags.sort_by(|a, b| b.ts.partial_cmp(&a.ts).unwrap()); + // println!("{:?}", tags); + + tags.drain(0.._parsed.keep as usize); + + println!("delete tag number : {}", tags.len()); + for x in tags.iter() { + match registry.delete_repository_tag(img.as_str(), x.digest.as_str()).await { + Ok(_) => println!("delete {} success", x.tag), + Err(_) => println!("delete {} fail", x.tag), + }; + } } }); } @@ -76,6 +110,14 @@ fn parse(input: &Vec) -> Result { Occur::Optional, None, ); + arg.option( + "k", + "keep", + "keep tags number", + "KEEP", + Occur::Optional, + None, + ); arg.parse(input).unwrap(); @@ -98,10 +140,16 @@ fn parse(input: &Vec) -> Result { let url: String = arg.value_of("registry").unwrap(); + let keep: i32 = match arg.value_of("keep") { + Ok(v) => v, + Err(_) => 10, + }; + let opts = Opts { image: image.clone(), exclude: exclude.clone(), registry: url, + keep, }; Ok(opts)