mirror of
https://github.com/anthonyoteri/dredge.git
synced 2026-06-05 15:26:53 -04:00
Add support for listing image tags
This commit is contained in:
+78
@@ -0,0 +1,78 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::error::ApiError;
|
||||||
|
|
||||||
|
/// Iterate over a paginated result set, collecting and returning the response
|
||||||
|
/// set.
|
||||||
|
///
|
||||||
|
/// The Docker Registry API specifies that when making a GET request, the
|
||||||
|
/// response will be paginated using a Link response header for the Next URI.
|
||||||
|
/// The URL will be encoded using RFC5988. [https://tools.ietf.org/html/rfc5988]
|
||||||
|
///
|
||||||
|
/// This function will continuously request the "Next" link as long as it is
|
||||||
|
/// returned, collecting and returning the deserialized response bodies as a
|
||||||
|
/// Vec<T>.
|
||||||
|
///
|
||||||
|
/// # Errors:
|
||||||
|
///
|
||||||
|
/// Returns an `ApiError` if there is a problem constructing the URL from the
|
||||||
|
/// configured `registry_url` base and the given `path`, or if there is an
|
||||||
|
/// error deserializing the HTTP response body as JSON, or if there is an
|
||||||
|
/// error parsing the `Link` header value as an RFC5988 URL.
|
||||||
|
pub async fn fetch_all<T: for<'de> Deserialize<'de>>(
|
||||||
|
config: &Config,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<Vec<T>, ApiError> {
|
||||||
|
log::trace!("fetch_all({path:?})");
|
||||||
|
|
||||||
|
let mut responses: Vec<T> = Vec::default();
|
||||||
|
let mut uri = String::from(path);
|
||||||
|
loop {
|
||||||
|
log::debug!("GET {uri:?}");
|
||||||
|
let url = config.registry_url.join(&uri)?;
|
||||||
|
|
||||||
|
let resp = reqwest::get(url).await?;
|
||||||
|
let headers = resp.headers().to_owned();
|
||||||
|
responses.push(resp.json().await?);
|
||||||
|
|
||||||
|
if let Some(path) = parse_rfc5988(headers.get(http::header::LINK))? {
|
||||||
|
uri = path;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(responses)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given an optional header value possibly containing an RFC5988 formatted
|
||||||
|
/// URL, parse said URL into a `String`.
|
||||||
|
///
|
||||||
|
/// If the header_value does not contain a correctly formatted RFC5988 URL,
|
||||||
|
/// or if the header_value is not properly formatted containing a URL
|
||||||
|
/// surrounded by angle brackets, separated from the link relation by a ';'
|
||||||
|
/// character, the `None` variant will be returned.
|
||||||
|
///
|
||||||
|
/// # Errors:
|
||||||
|
///
|
||||||
|
/// Returns and `ApiError` if there is a problem parsing contents of the
|
||||||
|
/// supplied header value.
|
||||||
|
fn parse_rfc5988(header_value: Option<&http::HeaderValue>) -> Result<Option<String>, ApiError> {
|
||||||
|
log::trace!("parse_rfc5988({header_value:?})");
|
||||||
|
|
||||||
|
if let Some(link_value) = header_value {
|
||||||
|
let link_str = link_value.to_str()?;
|
||||||
|
let parts: Vec<&str> = link_str.split(';').collect();
|
||||||
|
if let Some(url_part) = parts.first() {
|
||||||
|
if let Some(path) = url_part
|
||||||
|
.trim()
|
||||||
|
.strip_prefix('<')
|
||||||
|
.and_then(|s| s.strip_suffix('>'))
|
||||||
|
{
|
||||||
|
return Ok(Some(String::from(path)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
+17
-3
@@ -1,11 +1,12 @@
|
|||||||
#![allow(unused_imports)]
|
#![allow(unused_imports)]
|
||||||
|
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use std::ffi::OsString;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// Dredge is a command line tool for working with the Docker Registry
|
/// Dredge is a command line tool for working with the Docker Registry
|
||||||
/// V2 API.
|
/// V2 API.
|
||||||
@@ -26,7 +27,7 @@ pub(crate) struct Cli {
|
|||||||
value_name = "LEVEL",
|
value_name = "LEVEL",
|
||||||
num_args = 0..=1,
|
num_args = 0..=1,
|
||||||
default_value_t = LogLevel::Info,
|
default_value_t = LogLevel::Info,
|
||||||
default_missing_value="info",
|
default_missing_value = "info",
|
||||||
value_enum
|
value_enum
|
||||||
)]
|
)]
|
||||||
pub log_level: LogLevel,
|
pub log_level: LogLevel,
|
||||||
@@ -55,11 +56,24 @@ impl From<LogLevel> for log::LevelFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct TagsArgs {
|
||||||
|
/// The image name.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
num_args = 0..=1
|
||||||
|
)]
|
||||||
|
pub(crate) name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
/// Fetch the list of available repositories from the catalog.
|
/// Fetch the list of available repositories from the catalog.
|
||||||
Catalog,
|
Catalog,
|
||||||
|
|
||||||
|
/// Fetch the list of tags for a given image.
|
||||||
|
Tags(TagsArgs),
|
||||||
|
|
||||||
/// Perform a simple API Version check towards the configured registry
|
/// Perform a simple API Version check towards the configured registry
|
||||||
/// endpoint.
|
/// endpoint.
|
||||||
Check,
|
Check,
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod catalog;
|
pub mod catalog;
|
||||||
|
pub mod tags;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
|
|||||||
+4
-76
@@ -3,9 +3,11 @@
|
|||||||
//! The "catalog" command works with the Docker Registry API's "catalog"
|
//! The "catalog" command works with the Docker Registry API's "catalog"
|
||||||
//! entitity available at /v2/_catalog.
|
//! entitity available at /v2/_catalog.
|
||||||
//!
|
//!
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::api;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::error::ApiError;
|
use crate::error::ApiError;
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
/// Path to the Docker Registry API's "catalog" entity.
|
/// Path to the Docker Registry API's "catalog" entity.
|
||||||
const BASE_CATALOG_URI: &str = "/v2/_catalog";
|
const BASE_CATALOG_URI: &str = "/v2/_catalog";
|
||||||
@@ -27,7 +29,7 @@ pub async fn handler(config: &Config) -> Result<(), ApiError> {
|
|||||||
|
|
||||||
log::trace!("handler()");
|
log::trace!("handler()");
|
||||||
|
|
||||||
let responses: Vec<Response> = fetch_all(config, BASE_CATALOG_URI).await?;
|
let responses: Vec<Response> = api::fetch_all(config, BASE_CATALOG_URI).await?;
|
||||||
let repository_list: Vec<&str> = responses
|
let repository_list: Vec<&str> = responses
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|r| r.repositories.iter().map(String::as_str))
|
.flat_map(|r| r.repositories.iter().map(String::as_str))
|
||||||
@@ -39,77 +41,3 @@ pub async fn handler(config: &Config) -> Result<(), ApiError> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over a paginated result set, collecting and returning the response
|
|
||||||
/// set.
|
|
||||||
///
|
|
||||||
/// The Docker Registry API specifies that when making a GET request, the
|
|
||||||
/// response will be paginated using a Link response header for the Next URI.
|
|
||||||
/// The URL will be encoded using RFC5988. [https://tools.ietf.org/html/rfc5988]
|
|
||||||
///
|
|
||||||
/// This function will continuously request the "Next" link as long as it is
|
|
||||||
/// returned, collecting and returning the deserialized response bodies as a
|
|
||||||
/// Vec<T>.
|
|
||||||
///
|
|
||||||
/// # Errors:
|
|
||||||
///
|
|
||||||
/// Returns an `ApiError` if there is a problem constructing the URL from the
|
|
||||||
/// configured `registry_url` base and the given `path`, or if there is an
|
|
||||||
/// error deserializing the HTTP response body as JSON, or if there is an
|
|
||||||
/// error parsing the `Link` header value as an RFC5988 URL.
|
|
||||||
async fn fetch_all<T: for<'de> serde::Deserialize<'de>>(
|
|
||||||
config: &Config,
|
|
||||||
path: &str,
|
|
||||||
) -> Result<Vec<T>, ApiError> {
|
|
||||||
log::trace!("fetch_all({path:?})");
|
|
||||||
|
|
||||||
let mut responses: Vec<T> = Vec::default();
|
|
||||||
let mut uri = String::from(path);
|
|
||||||
loop {
|
|
||||||
log::debug!("GET {uri:?}");
|
|
||||||
let url = config.registry_url.join(&uri)?;
|
|
||||||
|
|
||||||
let resp = reqwest::get(url).await?;
|
|
||||||
let headers = resp.headers().to_owned();
|
|
||||||
responses.push(resp.json().await?);
|
|
||||||
|
|
||||||
if let Some(path) = parse_rfc5988(headers.get(http::header::LINK))? {
|
|
||||||
uri = path;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(responses)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given an optional header value possibly containing an RFC5988 formatted
|
|
||||||
/// URL, parse said URL into a `String`.
|
|
||||||
///
|
|
||||||
/// If the header_value does not contain a correctly formatted RFC5988 URL,
|
|
||||||
/// or if the header_value is not properly formatted containing a URL
|
|
||||||
/// surrounded by angle brackets, separated from the link relation by a ';'
|
|
||||||
/// character, the `None` variant will be returned.
|
|
||||||
///
|
|
||||||
/// # Errors:
|
|
||||||
///
|
|
||||||
/// Returns and `ApiError` if there is a problem parsing contents of the
|
|
||||||
/// supplied header value.
|
|
||||||
fn parse_rfc5988(header_value: Option<&http::HeaderValue>) -> Result<Option<String>, ApiError> {
|
|
||||||
log::trace!("parse_rfc5988({header_value:?})");
|
|
||||||
|
|
||||||
if let Some(link_value) = header_value {
|
|
||||||
let link_str = link_value.to_str()?;
|
|
||||||
let parts: Vec<&str> = link_str.split(';').collect();
|
|
||||||
if let Some(url_part) = parts.first() {
|
|
||||||
if let Some(path) = url_part
|
|
||||||
.trim()
|
|
||||||
.strip_prefix('<')
|
|
||||||
.and_then(|s| s.strip_suffix('>'))
|
|
||||||
{
|
|
||||||
return Ok(Some(String::from(path)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
//! Command module responsible for handling the "tags" command.
|
||||||
|
//!
|
||||||
|
//! The "tags" command works with the Docker Registry API's "tags"
|
||||||
|
//! entitity available at /v2/<name>/tags/list.
|
||||||
|
//!
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::api;
|
||||||
|
use crate::cli::TagsArgs;
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::error::ApiError;
|
||||||
|
|
||||||
|
/// Path to the Docker Registry API's "catalog" entity.
|
||||||
|
const BASE_TAGS_URI: &str = "/v2/{name}/tags/list";
|
||||||
|
|
||||||
|
/// Handler for the `Tags` endpoint
|
||||||
|
///
|
||||||
|
/// Fetch the list of tags names for a given image from the Docker Registry API, and
|
||||||
|
/// simply print the resulting names to stdout.
|
||||||
|
///
|
||||||
|
/// # Errors:
|
||||||
|
///
|
||||||
|
/// Returns an `ApiError` if there is a problem fetching or parsing the
|
||||||
|
/// responses from the Docker Registry API.
|
||||||
|
pub async fn handler(config: &Config, args: &TagsArgs) -> Result<(), ApiError> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Response {
|
||||||
|
name: String,
|
||||||
|
tags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!("handler()");
|
||||||
|
|
||||||
|
let name = args.name.clone();
|
||||||
|
let url = BASE_TAGS_URI.replace("{name}", &name);
|
||||||
|
let responses: Vec<Response> = api::fetch_all(config, &url).await?;
|
||||||
|
let tag_list: Vec<&str> = responses
|
||||||
|
.iter()
|
||||||
|
.flat_map(|r| r.tags.iter().map(String::as_str))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for tag in tag_list {
|
||||||
|
println!("{tag}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+4
-2
@@ -1,9 +1,11 @@
|
|||||||
use crate::error::ConfigError;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::error::ConfigError;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub registry_url: Url,
|
pub registry_url: Url,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#![allow(clippy::enum_variant_names)]
|
#![allow(clippy::enum_variant_names)]
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// The common error type for this Application.
|
/// The common error type for this Application.
|
||||||
|
|||||||
+7
-5
@@ -1,12 +1,15 @@
|
|||||||
|
use std::ffi::OsString;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
use crate::cli::Cli;
|
use crate::cli::Cli;
|
||||||
use crate::cli::Commands;
|
use crate::cli::Commands;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::error::ConfigError;
|
use crate::error::ConfigError;
|
||||||
use crate::error::DredgeError;
|
use crate::error::DredgeError;
|
||||||
use clap::Parser;
|
|
||||||
use std::ffi::OsString;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
|
mod api;
|
||||||
pub(crate) mod cli;
|
pub(crate) mod cli;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
@@ -82,11 +85,10 @@ async fn main() -> Result<(), DredgeError> {
|
|||||||
locate_config_file(args.config).map_or_else(create_default_config_file, Ok)?;
|
locate_config_file(args.config).map_or_else(create_default_config_file, Ok)?;
|
||||||
log::debug!("Using configuration file {config_file:?}");
|
log::debug!("Using configuration file {config_file:?}");
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
let config = Config::try_from(config_file.as_ref())?;
|
let config = Config::try_from(config_file.as_ref())?;
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Commands::Catalog => commands::catalog::handler(&config).await?,
|
Commands::Catalog => commands::catalog::handler(&config).await?,
|
||||||
|
Commands::Tags(args) => commands::tags::handler(&config, &args).await?,
|
||||||
Commands::Check => commands::version::handler(&config).await?,
|
Commands::Check => commands::version::handler(&config).await?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user