From 3768401faf8f1ba1daf0afa07e76312e47c01246 Mon Sep 17 00:00:00 2001 From: Anthony Oteri Date: Wed, 20 Sep 2023 16:38:24 -0400 Subject: [PATCH] Refactoring for Show/Delete handlers Refactor the existing code architecture to be a little flatter to support adding the show and delete handlers. Currently these two handlers are just stubbed functions, but the CLI looks about right. --- src/api.rs | 67 +++++++++++++++++++++++ src/cli.rs | 13 +++++ src/commands.rs | 117 ++++++++++++++++++++++++++++++++++++++-- src/commands/catalog.rs | 59 -------------------- src/commands/tags.rs | 60 --------------------- src/commands/version.rs | 110 ------------------------------------- src/main.rs | 10 ++-- 7 files changed, 201 insertions(+), 235 deletions(-) delete mode 100644 src/commands/catalog.rs delete mode 100644 src/commands/tags.rs delete mode 100644 src/commands/version.rs diff --git a/src/api.rs b/src/api.rs index 3b687b9..7c79611 100644 --- a/src/api.rs +++ b/src/api.rs @@ -92,3 +92,70 @@ fn parse_rfc5988(header_value: Option<&http::HeaderValue>) -> Result 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/cli.rs b/src/cli.rs index 10d5184..cffdbdf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -78,8 +78,21 @@ pub enum Commands { Catalog, /// Fetch the list of tags for a given image. + #[command(arg_required_else_help = true)] Tags { name: String }, + /// Show detailed information about a particular image. + #[command(arg_required_else_help = true)] + Show { + image: String, + #[arg(default_missing_value = "latest")] + tag: Option, + }, + + /// Delete a tagged image from the registry. + #[command(arg_required_else_help = true)] + Delete { image: String, tag: String }, + /// Perform a simple API Version check towards the configured registry /// endpoint. Check, diff --git a/src/commands.rs b/src/commands.rs index 65f432c..462c4dd 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -14,6 +14,117 @@ * limitations under the License. */ -pub mod catalog; -pub mod tags; -pub mod version; +use crate::api; +use crate::config::Config; +use crate::error::ApiError; +use serde::Deserialize; + +/// Handler for the `Catalog` endpoint +/// +/// Fetch the list of repository names 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 catalog_handler(config: &Config) -> Result<(), ApiError> { + #[derive(Deserialize)] + struct Response { + repositories: Vec, + } + + log::trace!("catalog_handler()"); + let path = "v2/_catalog"; + + let responses: Vec = api::fetch_all(config, path).await?; + let repository_list: Vec<&str> = responses + .iter() + .flat_map(|r| r.repositories.iter().map(String::as_str)) + .collect(); + + for repository in repository_list { + println!("{repository}"); + } + + Ok(()) +} + +/// 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 tags_handler(config: &Config, name: &str) -> Result<(), ApiError> { + #[derive(Deserialize)] + struct Response { + tags: Vec, + } + + log::trace!("tags_handler(name: {name})"); + let path = format!("/v2/{name}/tags/list"); + + let responses: Vec = api::fetch_all(config, &path).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(()) +} + +/// Handler function for showing manifest details +/// +/// # Errors: +/// +/// Returns an `ApiError` if there is a problem fetching the manifest or if there +/// is a problem parsing the response from the Docker Registry API. +pub async fn show_handler(config: &Config, image: &str, tag: &str) -> Result<(), ApiError> { + log::trace!("show_handler(image: {image}, tag: {tag})"); + let base = config.registry_url.to_owned(); + let path = format!("/v2/{image}/manifests/{tag}"); + let _url = base.join(&path)?; + Ok(()) +} + +/// Handler function for deleting a manifest for a given tagged image. +/// +/// # Errors: +/// +/// Returns and `ApiError` if there is a problem converting the given tag to a +/// manifest digest, or if there is a problem deleting the manifest from the +/// Docker Registry API. +pub async fn delete_handler(_config: &Config, image: &str, tag: &str) -> Result<(), ApiError> { + log::trace!("delete_handler(image: {image}, tag: {tag})"); + todo!() +} + +// Path to the Docker Registry APIs "api version check" endpoint. + +/// 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 check_handler(config: &Config) -> Result<(), ApiError> { + log::trace!("check_handler()"); + + let base = config.registry_url.to_owned(); + let path = "/v2"; + let url = base.join(path)?; + + let response = reqwest::get(url).await?; + + api::parse_response_status(&response)?; + println!("Ok"); + Ok(()) +} diff --git a/src/commands/catalog.rs b/src/commands/catalog.rs deleted file mode 100644 index a6fdd23..0000000 --- a/src/commands/catalog.rs +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2023 Anthony Oteri - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -//! Command module responsible for handling the "catalog" command. -//! -//! The "catalog" command works with the Docker Registry APIs "catalog" -//! entity available at /v2/_catalog. -//! -use serde::Deserialize; - -use crate::api; -use crate::config::Config; -use crate::error::ApiError; - -/// Path to the Docker Registry APIs "catalog" entity. -const BASE_CATALOG_URI: &str = "/v2/_catalog"; - -/// Handler for the `Catalog` endpoint -/// -/// Fetch the list of repository names 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) -> Result<(), ApiError> { - #[derive(Deserialize)] - struct Response { - repositories: Vec, - } - - log::trace!("handler()"); - - let responses: Vec = api::fetch_all(config, BASE_CATALOG_URI).await?; - let repository_list: Vec<&str> = responses - .iter() - .flat_map(|r| r.repositories.iter().map(String::as_str)) - .collect(); - - for repository in repository_list { - println!("{repository}"); - } - - Ok(()) -} diff --git a/src/commands/tags.rs b/src/commands/tags.rs deleted file mode 100644 index 4ddb8b8..0000000 --- a/src/commands/tags.rs +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2023 Anthony Oteri - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -//! Command module responsible for handling the "tags" command. -//! -//! The "tags" command works with the Docker Registry APIs "tags" -//! entity available at /v2//tags/list. -//! -use serde::Deserialize; - -use crate::api; -use crate::config::Config; -use crate::error::ApiError; - -/// Path to the Docker Registry APIs "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, name: &str) -> Result<(), ApiError> { - #[derive(Deserialize)] - struct Response { - tags: Vec, - } - - log::trace!("handler()"); - - let url = BASE_TAGS_URI.replace("{name}", name); - let responses: Vec = 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!("{name}:{tag}"); - } - - Ok(()) -} diff --git a/src/commands/version.rs b/src/commands/version.rs deleted file mode 100644 index 7402c17..0000000 --- a/src/commands/version.rs +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2023 Anthony Oteri - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -//! Command module responsible for handling the API Version check. -//! -//! This is a minimal endpoint suitable for ensuring that the configured -//! Docker Registry API supports the correct API version. -//! -use crate::config::Config; -use crate::error::ApiError; - -/// Path to the Docker Registry APIs "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/main.rs b/src/main.rs index d7ff806..e14b877 100644 --- a/src/main.rs +++ b/src/main.rs @@ -103,9 +103,13 @@ async fn main() -> Result<(), DredgeError> { let config = Config::try_from(config_file.as_ref())?; match args.command { - Commands::Catalog => commands::catalog::handler(&config).await?, - Commands::Tags { name } => commands::tags::handler(&config, &name).await?, - Commands::Check => commands::version::handler(&config).await?, + Commands::Catalog => commands::catalog_handler(&config).await?, + Commands::Tags { name } => commands::tags_handler(&config, &name).await?, + Commands::Show { image, tag } => { + commands::show_handler(&config, &image, &tag.unwrap_or("latest".to_string())).await? + } + Commands::Delete { image, tag } => commands::delete_handler(&config, &image, &tag).await?, + Commands::Check => commands::check_handler(&config).await?, } Ok(())