From 2b50fdcab7d7a29b4b978d97aa5f5a284e26d648 Mon Sep 17 00:00:00 2001 From: Yezzi Hsueh Date: Sat, 11 Nov 2023 02:32:43 +0800 Subject: [PATCH] feat: add docker registry api support --- Cargo.lock | 22 ++++------ Cargo.toml | 3 +- src/bin/dockertags.rs | 62 +++++++--------------------- src/docker.rs | 95 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 47 ++++----------------- src/registry.rs | 67 ++++++++++++++++++++++++++++++ 6 files changed, 196 insertions(+), 100 deletions(-) create mode 100644 src/docker.rs create mode 100644 src/registry.rs diff --git a/Cargo.lock b/Cargo.lock index c894829..0b72664 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,7 +201,6 @@ dependencies = [ "reqwest", "serde", "serde_json", - "tokio", ] [[package]] @@ -274,6 +273,12 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + [[package]] name = "futures-sink" version = "0.3.29" @@ -293,9 +298,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-core", + "futures-io", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -893,21 +901,9 @@ dependencies = [ "num_cpus", "pin-project-lite", "socket2 0.5.5", - "tokio-macros", "windows-sys", ] -[[package]] -name = "tokio-macros" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tokio-native-tls" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index e4c9524..574a22d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,7 @@ authors = ["Yezzi Hsueh"] license = "MIT" [dependencies] -reqwest = "0.11.22" -tokio = { version = "1.34.0", features = ["macros", "rt-multi-thread", "rt"] } +reqwest = { version = "0.11.22", features = ["blocking"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } clap = { version = "4.4.7", features = ["derive"] } \ No newline at end of file diff --git a/src/bin/dockertags.rs b/src/bin/dockertags.rs index a7ab34b..e81e939 100644 --- a/src/bin/dockertags.rs +++ b/src/bin/dockertags.rs @@ -1,14 +1,14 @@ -use std::error::Error; -use reqwest; use clap::{arg, Parser}; -use docker_tags::{DockerResponse, DockerResult}; +//use docker_tags::docker::DockerHubTagsFetcher; +use docker_tags::{DockerTags, QueryArgs}; +use docker_tags::registry::RegistryTagsFetcher; #[derive(Parser, Debug)] #[command(name = "dockertags", author, version, about = "List all tags for a Docker image on a remote registry", long_about = "List all tags for a Docker image on a remote registry")] struct Args { /// docker image name #[arg(value_name = "REPOSITORY")] - image: Option, + repository: Option, /// docker image architecture #[arg(short, long, value_name = "ARCHITECTURE")] arch: Option, @@ -17,51 +17,19 @@ struct Args { name: Option, } -async fn get_images(namespace: &str, repository: &str) -> Result, Box> { - let page = 0; - let page_size = 1000; - let mut url = format!("https://hub.docker.com/v2/namespaces/{namespace}/repositories/{repository}/tags?page={page}&page_size={page_size}"); - let mut results: Vec = Vec::new(); - loop { - let text = reqwest::get(url).await? - .text().await?; - let mut response: DockerResponse = serde_json::from_str(&text).unwrap(); - results.append(&mut response.results); - url = match response.next { - None => { break; } - Some(next) => { next } - }; - } - // println!("{:?}", results); - Ok(results) -} - -#[tokio::main] -async fn main() { +fn main() { let args = Args::parse(); - let mut namespace = String::from("library"); - let mut repository = args.image.unwrap(); - let split = repository.split('/').collect::>(); - if split.len() > 1 { - namespace = split.get(0).unwrap().to_string(); - repository = split.get(1).unwrap().to_string(); - } - let results = get_images(&namespace, &repository).await.unwrap(); - let mut filtered = results.into_iter() - .filter(|x| !x.images.is_empty()) - .collect::>(); - if args.name.is_some() { - let pat = args.name.unwrap(); - filtered = filtered.into_iter().filter(|x| x.name.clone().unwrap().contains(&pat)) - .collect::>(); + let query_args = QueryArgs { + repository: args.repository, + name: args.name, + arch: args.arch, }; - if args.arch.is_some() { - let pat = args.arch.unwrap(); - filtered = filtered.into_iter().filter(|x| x.images.get(0).as_ref().unwrap().architecture.clone().unwrap().contains(&pat)) - .collect::>(); - }; - for result in filtered { - println!("{}", result.name.unwrap()) + // let docker_tags_fetcher = DockerHubTagsFetcher {}; + let docker_tags_fetcher = RegistryTagsFetcher {}; + let tags = docker_tags_fetcher.get_tags(&query_args); + + for tag in tags { + println!("{}", &tag) } } diff --git a/src/docker.rs b/src/docker.rs new file mode 100644 index 0000000..7f21869 --- /dev/null +++ b/src/docker.rs @@ -0,0 +1,95 @@ +use serde::{Deserialize, Serialize}; +use crate::{DockerTags, QueryArgs}; + +#[derive(Serialize, Deserialize)] +struct DockerResult { + content_type: Option, + creator: u64, + full_size: u64, + id: u64, + last_updated: Option, + last_updater: u64, + last_updater_username: Option, + media_type: Option, + name: Option, + repository: u32, + tag_last_pulled: Option, + tag_last_pushed: Option, + tag_status: Option, + v2: bool, + images: Vec, +} + +#[derive(Serialize, Deserialize)] +struct DockerImage { + architecture: Option, + digest: Option, + features: Option, + last_pulled: Option, + last_pushed: Option, + os: Option, + os_features: Option, + os_version: Option, + size: u64, + status: Option, + variant: Option, +} + +#[derive(Serialize, Deserialize)] +struct DockerResponse { + count: u32, + next: Option, + previous: Option, + results: Vec, +} + +pub struct DockerHubTagsFetcher {} + +impl DockerTags for DockerHubTagsFetcher { + fn get_tags(&self, args: &QueryArgs) -> Vec { + let page = 0; + let page_size = 1000; + + let mut namespace = "library"; + // let mut repository = args.repository.clone().unwrap(); + let repository = match &args.repository { + Some(s) => { + let vec = s.split('/') + .into_iter() + .collect::>(); + namespace = vec[0]; + vec[1] + } + None => { "" } + }; + let mut url = format!("https://hub.docker.com/v2/namespaces/{namespace}/repositories/{repository}/tags?page={page}&page_size={page_size}"); + let mut results: Vec = Vec::new(); + loop { + let text = reqwest::blocking::get(url).unwrap().text().unwrap(); + let mut response: DockerResponse = serde_json::from_str(&text).unwrap(); + results.append(&mut response.results); + url = match response.next { + None => { break; } + Some(next) => { next } + }; + } + let mut filtered = results.into_iter() + .filter(|x| !x.images.is_empty()) + .collect::>(); + if args.name.is_some() { + let pat = args.name.clone().unwrap(); + filtered = filtered.into_iter().filter(|x| x.name.clone().unwrap().contains(&pat)) + .collect::>(); + }; + if args.arch.is_some() { + let pat = args.arch.clone().unwrap(); + filtered = filtered.into_iter().filter(|x| x.images.get(0).as_ref().unwrap().architecture.clone().unwrap().contains(&pat)) + .collect::>(); + }; + let mut results = Vec::::new(); + for x in &filtered { + results.push(x.name.clone().unwrap()); + } + results + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 420d9de..3ea48c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,43 +1,14 @@ -use serde::{Deserialize, Serialize}; +pub mod registry; +pub mod docker; -#[derive(Serialize, Deserialize, Debug)] -pub struct DockerResult { - pub content_type: Option, - pub creator: u64, - pub full_size: u64, - pub id: u64, - pub last_updated: Option, - pub last_updater: u64, - pub last_updater_username: Option, - pub media_type: Option, +/// query args +pub struct QueryArgs { + pub repository: Option, pub name: Option, - pub repository: u32, - pub tag_last_pulled: Option, - pub tag_last_pushed: Option, - pub tag_status: Option, - pub v2: bool, - pub images: Vec, + pub arch: Option, } -#[derive(Serialize, Deserialize, Debug)] -pub struct DockerImage { - pub architecture: Option, - pub digest: Option, - pub features: Option, - pub last_pulled: Option, - pub last_pushed: Option, - pub os: Option, - pub os_features: Option, - pub os_version: Option, - pub size: u64, - pub status: Option, - pub variant: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct DockerResponse { - pub count: u32, - pub next: Option, - pub previous: Option, - pub results: Vec, +pub trait DockerTags { + /// query tags + fn get_tags(&self, args: &QueryArgs) -> Vec; } \ No newline at end of file diff --git a/src/registry.rs b/src/registry.rs new file mode 100644 index 0000000..82c5862 --- /dev/null +++ b/src/registry.rs @@ -0,0 +1,67 @@ +use serde::{Deserialize, Serialize}; +use crate::{DockerTags, QueryArgs}; + +/// registry api response struct +#[derive(Serialize, Deserialize)] +struct RegistryTagsResponse { + name: Option, + tags: Option>, +} + +#[derive(Serialize, Deserialize)] +struct RegistryTokenResponse { + token: Option, + access_token: Option, + expires_in: u32, + issued_at: Option, +} + + +pub struct RegistryTagsFetcher {} + +impl DockerTags for RegistryTagsFetcher { + fn get_tags(&self, args: &QueryArgs) -> Vec { + let mut namespace = "library"; + let repository = match &args.repository { + Some(s) => { + let vec = s.split('/') + .into_iter() + .collect::>(); + namespace = vec[0]; + vec[1] + } + None => { "" } + }; + let token = get_token(namespace, repository).unwrap(); + let url = format!("https://registry-1.docker.io/v2/{namespace}/{repository}/tags/list"); + let client = reqwest::blocking::Client::new(); + let response = client.get(url) + .header("Authorization", format!("Bearer {token}")) + .send(); + let mut results = Vec::::new(); + if let Ok(r) = response { + let json: RegistryTagsResponse = serde_json::from_str(r.text().unwrap().as_str()).unwrap(); + results.append(&mut json.tags.unwrap()); + }; + results + } +} + +fn get_token(namespace: &str, repository: &str) -> reqwest::Result { + let username = "simaek"; + let password = "snow@PX1314"; + let service = "registry.docker.io"; + let scope = format!("repository:{namespace}/{repository}:pull"); + let auth = format!("https://auth.docker.io/token?service={service}&scope={scope}"); + let client = reqwest::blocking::Client::new(); + let response = client.get(auth) + .basic_auth(username, Some(password)) + .send(); + match response { + Ok(r) => { + let json: RegistryTokenResponse = serde_json::from_str(r.text().unwrap().as_str()).unwrap(); + Ok(json.token.unwrap()) + } + Err(e) => { Err(e) } + } +}