Compare commits
5 Commits
9e44df4973
...
be99d9c21a
Author | SHA1 | Date | |
---|---|---|---|
be99d9c21a | |||
fa9aa4e74b | |||
a25f67987c | |||
2fa67bbcbc | |||
770a4180d1 |
@ -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);
|
||||
}
|
||||
|
@ -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()
|
27
src/lib.rs
27
src/lib.rs
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
121
src/registry.rs
121
src/registry.rs
@ -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,
|
||||
}
|
27
src/registry/docker_auth.rs
Normal file
27
src/registry/docker_auth.rs
Normal 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 }
|
||||
}
|
||||
}
|
49
src/registry/docker_config.rs
Normal file
49
src/registry/docker_config.rs
Normal 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)..]),
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user