diff --git a/src/cli.rs b/src/cli.rs index c67622a..fa63c56 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,13 +7,16 @@ use clap::ValueEnum; use std::ffi::OsString; use std::path::PathBuf; +/// Dredge is a command line tool for working with the Docker Registry +/// V2 API. #[derive(Debug, Parser)] #[command(name = "dredge", version, author)] -#[command(about = "A Docker Registry CLI tool", long_about = None)] +#[command(about, long_about)] pub(crate) struct Cli { #[command(subcommand)] pub command: Commands, + /// Optional configuration file override. #[arg(short = 'c', long = "config")] pub config: Option, @@ -54,5 +57,10 @@ impl From for log::LevelFilter { #[derive(Debug, Subcommand)] pub enum Commands { + /// Fetch the list of available repositories from the catalog. Catalog, + + /// Perform a simple API Version check towards the configured registry + /// endpoint. + Check, } diff --git a/src/commands.rs b/src/commands.rs index 1b1f77c..f97e78e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1 +1,2 @@ pub mod catalog; +pub mod version; diff --git a/src/commands/version.rs b/src/commands/version.rs new file mode 100644 index 0000000..4b328bb --- /dev/null +++ b/src/commands/version.rs @@ -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(), + )), + } +} diff --git a/src/error.rs b/src/error.rs index 14c450f..422ee73 100644 --- a/src/error.rs +++ b/src/error.rs @@ -60,6 +60,18 @@ pub enum ApiError { #[error("Failed to parse response headers")] ResponseHeaderParseError(Box), + + #[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 for ApiError { diff --git a/src/main.rs b/src/main.rs index 12d35c8..b1b8a62 100644 --- a/src/main.rs +++ b/src/main.rs @@ -87,6 +87,7 @@ async fn main() -> Result<(), DredgeError> { match args.command { Commands::Catalog => commands::catalog::handler(&config).await?, + Commands::Check => commands::version::handler(&config).await?, } Ok(())