Compare commits

..

5 Commits

6 changed files with 169 additions and 130 deletions

View File

@ -1,35 +1,39 @@
use std::io::Write;
use clap::{arg, Parser};
//use docker_tags::docker::DockerHubTagsFetcher;
use docker_tags::{DockerTags, QueryArgs};
use docker_tags::registry::RegistryTagsFetcher;
use docker_tags::docker_hub::DockerHubTagsFetcher;
use docker_tags::{DockerTagsFetcher, QueryArgs};
use docker_tags::registry::{docker_config, RegistryTagsFetcher};
#[derive(Parser, Debug)]
#[derive(Parser)]
#[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")]
repository: Option<String>,
repository: String,
/// docker image architecture
#[arg(short, long, value_name = "ARCHITECTURE")]
arch: Option<String>,
/// image tags name to filter
#[arg(short, long, value_name = "TAG")]
name: Option<String>,
filter: Option<String>,
}
fn main() {
let args = Args::parse();
let query_args = QueryArgs {
repository: args.repository,
name: args.name,
arch: args.arch,
let query_args = QueryArgs::new(args.repository.as_str(), args.filter, args.arch);
let config = docker_config::read_config();
let mut tags = match config {
None => {
let fetcher = DockerHubTagsFetcher::new();
fetcher.get_tags(&query_args)
}
Some(x) => {
let fetcher = RegistryTagsFetcher::new(&x);
fetcher.get_tags(&query_args)
}
};
// let docker_tags_fetcher = DockerHubTagsFetcher {};
let docker_tags_fetcher = RegistryTagsFetcher {};
let tags = docker_tags_fetcher.get_tags(&query_args);
tags.sort();
for tag in tags {
let _ = writeln!(std::io::stdout(), "{}", &tag);
}

View File

@ -1,5 +1,6 @@
use std::io::Write;
use serde::{Deserialize, Serialize};
use crate::{DockerTags, QueryArgs};
use crate::{DockerTagsFetcher, QueryArgs};
#[derive(Serialize, Deserialize)]
struct DockerResult {
@ -45,32 +46,34 @@ struct DockerResponse {
pub struct DockerHubTagsFetcher {}
impl DockerTags for DockerHubTagsFetcher {
impl DockerHubTagsFetcher {
pub fn new() -> DockerHubTagsFetcher {
DockerHubTagsFetcher {}
}
}
impl DockerTagsFetcher for DockerHubTagsFetcher {
fn get_tags(&self, args: &QueryArgs) -> Vec<String> {
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::<Vec<&str>>();
namespace = vec[0];
vec[1]
}
None => { "" }
};
let namespace = args.namespace.as_str();
let repository = args.repository.as_str();
let mut url = format!("https://hub.docker.com/v2/namespaces/{namespace}/repositories/{repository}/tags?page={page}&page_size={page_size}");
let mut results: Vec<DockerResult> = 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 resp = reqwest::blocking::get(&url).unwrap();
if resp.status().is_success() {
let text = resp.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 }
};
} else {
let _x = writeln!(std::io::stdout(), "Image not found: {}/{}", namespace, repository);
break;
};
}
let mut filtered = results.into_iter()

View File

@ -1,14 +1,33 @@
pub mod registry;
pub mod docker;
pub mod docker_hub;
/// query args
pub struct QueryArgs {
pub repository: Option<String>,
pub namespace: String,
pub repository: String,
pub name: Option<String>,
pub arch: Option<String>,
}
pub trait DockerTags {
pub trait DockerTagsFetcher {
/// query tags
fn get_tags(&self, args: &QueryArgs) -> Vec<String>;
}
}
impl QueryArgs {
pub fn new(repository: &str, name: Option<String>, arch: Option<String>) -> Self {
let mut repository = repository;
let mut namespace = "library";
let option = repository.find('/');
if option.is_some() {
namespace = &repository[0..option.unwrap()];
repository = &repository[(option.unwrap() + 1)..];
}
Self {
namespace: String::from(namespace),
repository: String::from(repository),
name,
arch,
}
}
}

View File

@ -1,8 +1,9 @@
use std::collections::HashMap;
use std::fs;
use directories::UserDirs;
pub mod docker_auth;
pub mod docker_config;
use serde::{Deserialize, Serialize};
use crate::{DockerTags, QueryArgs};
use crate::{DockerTagsFetcher, QueryArgs};
use crate::registry::docker_config::DockerPassword;
/// registry api response struct
#[derive(Serialize, Deserialize)]
@ -11,29 +12,25 @@ struct RegistryTagsResponse {
tags: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize)]
struct RegistryTokenResponse {
token: Option<String>,
access_token: Option<String>,
expires_in: u32,
issued_at: Option<String>,
pub struct RegistryTagsFetcher {
username: String,
password: String,
}
pub struct RegistryTagsFetcher {}
impl DockerTags for RegistryTagsFetcher {
fn get_tags(&self, args: &QueryArgs) -> Vec<String> {
let repository = args.repository.clone().unwrap();
let mut namespace = "library";
let mut repository = repository.as_str();
let option = repository.find('/');
if option.is_some() {
namespace = &repository[0..option.unwrap()];
repository = &repository[(option.unwrap() + 1)..];
impl RegistryTagsFetcher {
pub fn new(docker_password: &DockerPassword) -> RegistryTagsFetcher {
RegistryTagsFetcher {
username: docker_password.username.to_string(),
password: docker_password.password.to_string(),
}
let token = get_token(namespace, repository).unwrap();
}
}
impl DockerTagsFetcher for RegistryTagsFetcher {
fn get_tags(&self, args: &QueryArgs) -> Vec<String> {
let namespace = args.namespace.as_str();
let repository = args.repository.as_str();
let token = docker_auth::get_token(namespace, repository, self.username.as_str(), self.password.as_str()).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)
@ -42,76 +39,16 @@ impl DockerTags for RegistryTagsFetcher {
let mut results = Vec::<String>::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());
match json.tags {
Some(mut x) => { results.append(&mut x); }
None => { results.push(format!("Image not found: {}/{}", namespace, repository)); }
}
};
if args.name.is_some() {
let pat = args.name.clone().unwrap();
results = results.into_iter().filter(|x| x.contains(&pat))
.collect::<Vec<String>>();
};
results
}
}
/// get the access token for tags request
fn get_token(namespace: &str, repository: &str) -> Option<String> {
let config = match read_config() {
None => { return None; }
Some(x) => { x }
};
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(config.username, Some(config.password))
.send();
match response {
Ok(r) => {
let json: RegistryTokenResponse = serde_json::from_str(r.text().unwrap().as_str()).unwrap();
Some(json.token.unwrap())
}
Err(_) => { None }
}
}
#[derive(Serialize, Deserialize)]
struct DockerConfig {
auths: HashMap<String, DockerAuth>,
}
#[derive(Serialize, Deserialize)]
struct DockerAuth {
auth: Option<String>,
}
use base64::{Engine as _, engine::general_purpose};
/// read authentication info from local docker config file
fn read_config() -> Option<DockerPassword> {
let user_dirs = UserDirs::new().unwrap();
let home_dir = user_dirs.home_dir();
let config_path = home_dir.join(".docker").join("config.json");
let config_context = fs::read_to_string(config_path);
let docker_config: DockerConfig = serde_json::from_str(config_context.unwrap().as_str()).unwrap();
let auths = docker_config.auths;
let auth = match auths.get("https://index.docker.io/v1/") {
None => { return None; }
Some(docker_auth) => { docker_auth.auth.clone().unwrap() }
};
let mut buffer = Vec::<u8>::new();
let _result = general_purpose::URL_SAFE_NO_PAD.decode_vec(auth.as_bytes(), &mut buffer);
let decoded = match std::str::from_utf8(&buffer) {
Ok(v) => v,
Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
};
let idx = match decoded.find(':') {
None => { panic!("Invalid authentication: {}", decoded) }
Some(i) => { i }
};
return Some(DockerPassword {
username: String::from(&decoded[0..idx]),
password: String::from(&decoded[(idx + 1)..]),
});
}
#[derive(Serialize, Deserialize)]
struct DockerPassword {
username: String,
password: String,
}

View File

@ -0,0 +1,27 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct RegistryTokenResponse {
token: Option<String>,
access_token: Option<String>,
expires_in: u32,
issued_at: Option<String>,
}
/// get the access token for tags request
pub fn get_token(namespace: &str, repository: &str, username: &str, password: &str) -> Option<String> {
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();
Some(json.token.unwrap())
}
Err(_) => { None }
}
}

View File

@ -0,0 +1,49 @@
use std::collections::HashMap;
use std::fs;
use base64::{Engine as _, engine::general_purpose};
use directories::UserDirs;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct DockerConfig {
auths: HashMap<String, DockerAuth>,
}
#[derive(Serialize, Deserialize)]
struct DockerAuth {
auth: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct DockerPassword {
pub username: String,
pub password: String,
}
/// read authentication info from local docker config file
pub fn read_config() -> Option<DockerPassword> {
let user_dirs = UserDirs::new().unwrap();
let home_dir = user_dirs.home_dir();
let config_path = home_dir.join(".docker").join("config.json");
let config_context = fs::read_to_string(config_path);
let docker_config: DockerConfig = serde_json::from_str(config_context.unwrap().as_str()).unwrap();
let auths = docker_config.auths;
let auth = match auths.get("https://index.docker.io/v1/") {
None => { return None; }
Some(docker_auth) => { docker_auth.auth.clone().unwrap() }
};
let mut buffer = Vec::<u8>::new();
let _result = general_purpose::URL_SAFE_NO_PAD.decode_vec(auth.as_bytes(), &mut buffer);
let decoded = match std::str::from_utf8(&buffer) {
Ok(v) => v,
Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
};
let idx = match decoded.find(':') {
None => { panic!("Invalid authentication: {}", decoded) }
Some(i) => { i }
};
return Some(DockerPassword {
username: String::from(&decoded[0..idx]),
password: String::from(&decoded[(idx + 1)..]),
});
}