mirror of
https://github.com/anthonyoteri/dredge.git
synced 2026-06-05 15:26:53 -04:00
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.
This commit is contained in:
+67
@@ -92,3 +92,70 @@ fn parse_rfc5988(header_value: Option<&http::HeaderValue>) -> Result<Option<Stri
|
|||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
pub 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(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+13
@@ -78,8 +78,21 @@ pub enum Commands {
|
|||||||
Catalog,
|
Catalog,
|
||||||
|
|
||||||
/// Fetch the list of tags for a given image.
|
/// Fetch the list of tags for a given image.
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
Tags { name: String },
|
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<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// 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
|
/// Perform a simple API Version check towards the configured registry
|
||||||
/// endpoint.
|
/// endpoint.
|
||||||
Check,
|
Check,
|
||||||
|
|||||||
+114
-3
@@ -14,6 +14,117 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pub mod catalog;
|
use crate::api;
|
||||||
pub mod tags;
|
use crate::config::Config;
|
||||||
pub mod version;
|
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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!("catalog_handler()");
|
||||||
|
let path = "v2/_catalog";
|
||||||
|
|
||||||
|
let responses: Vec<Response> = 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
log::trace!("tags_handler(name: {name})");
|
||||||
|
let path = format!("/v2/{name}/tags/list");
|
||||||
|
|
||||||
|
let responses: Vec<Response> = 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(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -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<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
log::trace!("handler()");
|
|
||||||
|
|
||||||
let responses: Vec<Response> = 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(())
|
|
||||||
}
|
|
||||||
@@ -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/<name>/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<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
log::trace!("handler()");
|
|
||||||
|
|
||||||
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!("{name}:{tag}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -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(),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+7
-3
@@ -103,9 +103,13 @@ async fn main() -> Result<(), DredgeError> {
|
|||||||
|
|
||||||
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 { name } => commands::tags::handler(&config, &name).await?,
|
Commands::Tags { name } => commands::tags_handler(&config, &name).await?,
|
||||||
Commands::Check => commands::version::handler(&config).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(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user