Add API Version check endpoint

This commit is contained in:
Anthony Oteri
2023-09-14 10:03:10 -04:00
parent 00a1ad87e7
commit 9445c9e817
5 changed files with 117 additions and 1 deletions
+9 -1
View File
@@ -7,13 +7,16 @@ use clap::ValueEnum;
use std::ffi::OsString; use std::ffi::OsString;
use std::path::PathBuf; use std::path::PathBuf;
/// Dredge is a command line tool for working with the Docker Registry
/// V2 API.
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(name = "dredge", version, author)] #[command(name = "dredge", version, author)]
#[command(about = "A Docker Registry CLI tool", long_about = None)] #[command(about, long_about)]
pub(crate) struct Cli { pub(crate) struct Cli {
#[command(subcommand)] #[command(subcommand)]
pub command: Commands, pub command: Commands,
/// Optional configuration file override.
#[arg(short = 'c', long = "config")] #[arg(short = 'c', long = "config")]
pub config: Option<OsString>, pub config: Option<OsString>,
@@ -54,5 +57,10 @@ impl From<LogLevel> for log::LevelFilter {
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
pub enum Commands { pub enum Commands {
/// Fetch the list of available repositories from the catalog.
Catalog, Catalog,
/// Perform a simple API Version check towards the configured registry
/// endpoint.
Check,
} }
+1
View File
@@ -1 +1,2 @@
pub mod catalog; pub mod catalog;
pub mod version;
+94
View File
@@ -0,0 +1,94 @@
//! Command module responsible for handling the API Version check.
//!
//! This is a minimal endpoint suitable for ensuring that the configured
//! Docker Regsitry API supports the correct API version.
//!
use crate::config::Config;
use crate::error::ApiError;
/// Path to the Docker Registry API's "api version check" endpoint.
const BASE_URL: &str = "/v2";
/// Handler for the API Version Check.
///
/// # Errors:
///
/// Returns an `ApiError` if there is a problem communicating with the
/// endpoint or if the required version is not supported.
pub async fn handler(config: &Config) -> Result<(), ApiError> {
log::trace!("handler()");
let url = config.registry_url.join(BASE_URL)?;
let response = reqwest::get(url).await?;
parse_response_status(&response)?;
println!("Ok");
Ok(())
}
/// Parse the response according to the API Documentation.
///
/// If a 200 OK response is returned, the registry implements the V2(.1)
/// registry API and the client may proceed safely with other V2 operations.
/// Optionally, the response may contain information about the supported
/// paths in the response body. The client should be prepared to ignore this data.
///
/// If a 401 Unauthorized response is returned, the client should take action
/// based on the contents of the "WWW-Authenticate" header and try the endpoint
/// again. Depending on access control setup, the client may still have to
/// authenticate against different resources, even if this check succeeds.
///
/// If 404 Not Found response status, or other unexpected status, is returned,
/// the client should proceed with the assumption that the registry does not
/// implement V2 of the API.
///
/// When a 200 OK or 401 Unauthorized response is returned, the
/// "Docker-Distribution-API-Version" header should be set to "registry/2.0".
/// Clients may require this header value to determine if the endpoint serves
/// this API. When this header is omitted, clients may fallback to an older
/// API version.
///
/// # Errors:
///
/// Returns an `ApiError` on the following conditions:
///
/// * There is an error parsing the "Docker-Distribution-API-Version" header.
/// * The value of the above header is not the expected result.
/// * The above header is missing from the response.
/// * A non 200 HTTP response status code is returned.
fn parse_response_status(response: &reqwest::Response) -> Result<(), ApiError> {
match response.status() {
http::StatusCode::OK => {
let headers = response.headers();
if let Some(header_value) = headers.get("Docker-Distribution-API-Version") {
if header_value.to_str()? != "registry/2.0" {
Err(ApiError::UnsupportedVersion(header_value.to_str()?.into()))
} else {
Ok(())
}
} else {
Err(ApiError::UnexpectedResponse(
"Missing version header".into(),
))
}
}
http::StatusCode::UNAUTHORIZED => {
let headers = response.headers();
if let Some(header_value) = headers.get("Docker-Distribution-API-Version") {
if header_value.to_str()? != "registry/2.0" {
Err(ApiError::UnsupportedVersion(header_value.to_str()?.into()))
} else {
Err(ApiError::AuthorizationFailed)
}
} else {
Err(ApiError::UnexpectedResponse(
"Missing version header".into(),
))
}
}
http::StatusCode::NOT_FOUND => Err(ApiError::NotFound),
_ => Err(ApiError::UnexpectedResponse(
"Undocumented status code".into(),
)),
}
}
+12
View File
@@ -60,6 +60,18 @@ pub enum ApiError {
#[error("Failed to parse response headers")] #[error("Failed to parse response headers")]
ResponseHeaderParseError(Box<dyn std::error::Error>), ResponseHeaderParseError(Box<dyn std::error::Error>),
#[error("Version Mismatch {0}")]
UnsupportedVersion(String),
#[error("Unexpected response from API: {0}")]
UnexpectedResponse(String),
#[error("HTTP Authorization failed")]
AuthorizationFailed,
#[error("Resource not found")]
NotFound,
} }
impl From<reqwest::header::ToStrError> for ApiError { impl From<reqwest::header::ToStrError> for ApiError {
+1
View File
@@ -87,6 +87,7 @@ async fn main() -> Result<(), DredgeError> {
match args.command { match args.command {
Commands::Catalog => commands::catalog::handler(&config).await?, Commands::Catalog => commands::catalog::handler(&config).await?,
Commands::Check => commands::version::handler(&config).await?,
} }
Ok(()) Ok(())