mirror of
https://github.com/anthonyoteri/dredge.git
synced 2026-06-05 15:26:53 -04:00
d2d51b3a2d
- api: extract check_api_version_header() helper, eliminating duplicated header-checking logic in parse_response_status() - api: simplify parse_rfc5988() using split_once and let-else - api: propagate JSON decode errors in fetch_paginated() instead of silently swallowing them - api: add connect/request timeouts via a shared build_client() helper; all handlers now use a configured client instead of reqwest::get() - api: fix stale log trace name get_manifest -> get_digest - commands: promote inline response structs to module-level for clarity - commands: fix etag stripping logic (was using wrong quote/apostrophe pattern; now correctly strips RFC 7232 double-quotes) - commands: simplify iterator chains in catalog/tags handlers - error: simplify ResponseHeaderParseError from Box<dyn Error> to String - main: fix stale log trace name make_registry_url -> parse_registry_arg - main: use as_deref().unwrap_or() instead of allocating via to_owned() - cli: remove unused imports and #![allow(unused_imports)] attribute
202 lines
6.6 KiB
Rust
202 lines
6.6 KiB
Rust
/*
|
|
* Copyright 2023 Anthony Oteri
|
|
*
|
|
* Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
|
|
* http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
|
|
* http://opensource.org/licenses/MIT>, at your option. This file may not be
|
|
* copied, modified, or distributed except according to those terms.
|
|
*/
|
|
|
|
#![deny(clippy::pedantic)]
|
|
|
|
use std::io::{self, Write};
|
|
|
|
use clap::Parser;
|
|
use simple_logger::SimpleLogger;
|
|
use url::Url;
|
|
|
|
use crate::cli::Cli;
|
|
use crate::cli::Commands;
|
|
use crate::error::DredgeError;
|
|
|
|
mod api;
|
|
pub(crate) mod cli;
|
|
mod commands;
|
|
mod error;
|
|
|
|
/// The default image tag used when no tag is specified by the caller.
|
|
const LATEST: &str = "latest";
|
|
|
|
/// Parse the `<REGISTRY>` CLI argument into a complete Docker Registry [`Url`].
|
|
///
|
|
/// Accepts a bare hostname (`registry.example.com`), a host-and-port pair
|
|
/// (`registry.example.com:5000`), or a full URL
|
|
/// (`https://registry.example.com:5000`). When no URL scheme is present,
|
|
/// `https://` is prepended automatically before parsing.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns [`DredgeError::RegistryUrlError`] containing the attempted URL
|
|
/// string if it cannot be parsed as a valid URL after the scheme is prepended.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust,ignore
|
|
/// // Bare hostname — HTTPS is assumed
|
|
/// let url = parse_registry_arg("registry.example.com").unwrap();
|
|
/// assert_eq!(url.scheme(), "https");
|
|
///
|
|
/// // Host with port
|
|
/// let url = parse_registry_arg("registry.example.com:5000").unwrap();
|
|
/// assert_eq!(url.port(), Some(5000));
|
|
///
|
|
/// // Full URL returned as-is
|
|
/// let url = parse_registry_arg("https://registry.example.com").unwrap();
|
|
/// assert_eq!(url.as_str(), "https://registry.example.com/");
|
|
/// ```
|
|
fn parse_registry_arg(host: &str) -> Result<Url, DredgeError> {
|
|
log::trace!("parse_registry_arg(host: {host})");
|
|
|
|
let mut host = String::from(host);
|
|
if !host.starts_with("http://") && !host.starts_with("https://") {
|
|
host = format!("https://{host}");
|
|
}
|
|
|
|
Url::parse(&host).or(Err(DredgeError::RegistryUrlError(host.clone())))
|
|
}
|
|
|
|
#[tokio::main(flavor = "current_thread")]
|
|
async fn main() -> Result<(), DredgeError> {
|
|
let args = Cli::parse();
|
|
|
|
// -- Initialize logging
|
|
let log_level = args.log_level;
|
|
SimpleLogger::new()
|
|
.with_colors(true)
|
|
.with_utc_timestamps()
|
|
.with_level(log_level.into())
|
|
.env()
|
|
.init()?;
|
|
|
|
// -- Parse the given <REGISTRY> argument into a complete URL
|
|
let registry_url: Url = parse_registry_arg(&args.registry)?;
|
|
|
|
// -- Dispatch control to the appropriate command handler.
|
|
let mut buf: Vec<u8> = Vec::new();
|
|
match args.command {
|
|
Commands::Catalog => commands::catalog_handler(&mut buf, ®istry_url).await?,
|
|
Commands::Tags { name } => commands::tags_handler(&mut buf, ®istry_url, &name).await?,
|
|
Commands::Show { image, tag } => {
|
|
commands::show_handler(
|
|
&mut buf,
|
|
®istry_url,
|
|
&image,
|
|
tag.as_deref().unwrap_or(LATEST),
|
|
)
|
|
.await?;
|
|
}
|
|
Commands::Delete { image, tag } => {
|
|
commands::delete_handler(&mut buf, ®istry_url, &image, &tag).await?;
|
|
}
|
|
Commands::Check => commands::check_handler(&mut buf, ®istry_url).await?,
|
|
}
|
|
|
|
io::stdout().write_all(&buf)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// Test that given a valid URL in the <REGISTRY> argument, we return the
|
|
/// same URL from `parse_registry_arg()`
|
|
#[test]
|
|
fn test_parse_valid_url_registry_arg() {
|
|
let host = "https://example.com/registry";
|
|
let result = parse_registry_arg(host);
|
|
|
|
// Check if the result is Ok and contains the expected URL
|
|
assert!(result.is_ok());
|
|
let url = result.unwrap();
|
|
assert_eq!(url.scheme(), "https");
|
|
assert_eq!(url.host_str(), Some("example.com"));
|
|
assert_eq!(url.path(), "/registry");
|
|
}
|
|
|
|
/// Test that given only an FQDN for a specific host in the <REGISTRY>
|
|
/// argument, we return an HTTPS url with that FQDN as the host.
|
|
#[test]
|
|
fn test_parse_valid_fqdn_registry_arg() {
|
|
let host = "example.com";
|
|
let result = parse_registry_arg(host);
|
|
|
|
// Check if the result is Ok and contains the expected URL
|
|
assert!(result.is_ok());
|
|
let url = result.unwrap();
|
|
assert_eq!(url.scheme(), "https");
|
|
assert_eq!(url.host_str(), Some("example.com"));
|
|
assert_eq!(url.path(), "/");
|
|
}
|
|
|
|
/// Test that given an FQDN with port for a specific host in the <REGISTRY>
|
|
/// argument, we return an HTTPS url with that FQDN as the host and the
|
|
/// given port as the parsed port number.
|
|
#[test]
|
|
fn test_parse_valid_fqdn_registry_arg_alt_port() {
|
|
let host = "example.com:5123";
|
|
let result = parse_registry_arg(host);
|
|
|
|
// Check if the result is Ok and contains the expected URL
|
|
assert!(result.is_ok());
|
|
let url = result.unwrap();
|
|
assert_eq!(url.scheme(), "https");
|
|
assert_eq!(url.host_str(), Some("example.com"));
|
|
assert_eq!(url.port(), Some(5123));
|
|
assert_eq!(url.path(), "/");
|
|
}
|
|
|
|
/// Test that given an arbitrary string which can not be parsed as a valid
|
|
/// URL or FQDN, we return the `RegistryUrlError` variant.
|
|
#[test]
|
|
fn test_parse_invalid_registry_arg() {
|
|
let host = "///"; // This is not a valid URL
|
|
let result = parse_registry_arg(host);
|
|
|
|
// Check if result is Err and matches the expected error variant.
|
|
assert!(result.is_err());
|
|
match result {
|
|
Err(DredgeError::RegistryUrlError(_)) => {} // Expected error variant,
|
|
_ => panic!("Expected RegistryUrlError, got a different error"),
|
|
}
|
|
}
|
|
|
|
/// Test that an HTTP (non-HTTPS) URL is returned as-is without prepending
|
|
/// the HTTPS scheme.
|
|
#[test]
|
|
fn test_parse_registry_arg_http_url() {
|
|
let host = "http://example.com/registry";
|
|
let result = parse_registry_arg(host);
|
|
|
|
assert!(result.is_ok());
|
|
let url = result.unwrap();
|
|
assert_eq!(url.scheme(), "http");
|
|
assert_eq!(url.host_str(), Some("example.com"));
|
|
assert_eq!(url.path(), "/registry");
|
|
}
|
|
|
|
/// Test that a trailing slash in the registry argument is preserved.
|
|
#[test]
|
|
fn test_parse_registry_arg_trailing_slash() {
|
|
let host = "example.com/registry/";
|
|
let result = parse_registry_arg(host);
|
|
|
|
assert!(result.is_ok());
|
|
let url = result.unwrap();
|
|
assert_eq!(url.scheme(), "https");
|
|
assert_eq!(url.host_str(), Some("example.com"));
|
|
assert_eq!(url.path(), "/registry/");
|
|
}
|
|
}
|