From 535e08e9bba8899d3dfcfb72bccadbc4ef852a2a Mon Sep 17 00:00:00 2001 From: Anthony Oteri Date: Thu, 21 Sep 2023 15:34:19 -0400 Subject: [PATCH] Support for specifying the URL on the command line --- src/api.rs | 6 ++-- src/cli.rs | 3 ++ src/commands.rs | 22 ++++++------- src/error.rs | 4 +++ src/main.rs | 87 ++++++++++++++++--------------------------------- 5 files changed, 48 insertions(+), 74 deletions(-) diff --git a/src/api.rs b/src/api.rs index 3c8ba5c..c2eb244 100644 --- a/src/api.rs +++ b/src/api.rs @@ -15,8 +15,8 @@ */ use serde::Deserialize; +use url::Url; -use crate::config::Config; use crate::error::ApiError; /// Iterate over a paginated result set, collecting and returning the response @@ -37,7 +37,7 @@ use crate::error::ApiError; /// error deserializing the HTTP response body as JSON, or if there is an /// error parsing the `Link` header value as an RFC5988 URL. pub async fn fetch_all Deserialize<'de>>( - config: &Config, + base: &Url, path: &str, ) -> Result, ApiError> { log::trace!("fetch_all({path:?})"); @@ -46,7 +46,7 @@ pub async fn fetch_all Deserialize<'de>>( let mut path = String::from(path); loop { log::debug!("GET {path:?}"); - let url = config.registry_url.join(&path)?; + let url = base.join(&path)?; let resp = reqwest::get(url).await?; let headers = resp.headers().clone(); diff --git a/src/cli.rs b/src/cli.rs index cffdbdf..a1255ea 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -47,6 +47,9 @@ pub(crate) struct Cli { value_enum )] pub log_level: LogLevel, + + /// The host or host:port for the Docker Registry + pub registry: String, } #[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] diff --git a/src/commands.rs b/src/commands.rs index df69589..0721c80 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -15,9 +15,9 @@ */ use serde::Deserialize; +use url::Url; use crate::api; -use crate::config::Config; use crate::error::ApiError; /// Handler for the `Catalog` endpoint @@ -29,7 +29,7 @@ use crate::error::ApiError; /// /// 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> { +pub async fn catalog_handler(registry_url: &Url) -> Result<(), ApiError> { #[derive(Deserialize)] struct Response { repositories: Vec, @@ -38,7 +38,7 @@ pub async fn catalog_handler(config: &Config) -> Result<(), ApiError> { log::trace!("catalog_handler()"); let path = "v2/_catalog"; - let responses: Vec = api::fetch_all(config, path).await?; + let responses: Vec = api::fetch_all(registry_url, path).await?; let repository_list: Vec<&str> = responses .iter() .flat_map(|r| r.repositories.iter().map(String::as_str)) @@ -60,7 +60,7 @@ pub async fn catalog_handler(config: &Config) -> Result<(), ApiError> { /// /// 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> { +pub async fn tags_handler(registry_url: &Url, name: &str) -> Result<(), ApiError> { #[derive(Deserialize)] struct Response { tags: Vec, @@ -69,7 +69,7 @@ pub async fn tags_handler(config: &Config, name: &str) -> Result<(), ApiError> { log::trace!("tags_handler(name: {name})"); let path = format!("/v2/{name}/tags/list"); - let responses: Vec = api::fetch_all(config, &path).await?; + let responses: Vec = api::fetch_all(registry_url, &path).await?; let tag_list: Vec<&str> = responses .iter() .flat_map(|r| r.tags.iter().map(String::as_str)) @@ -89,11 +89,10 @@ pub async fn tags_handler(config: &Config, name: &str) -> Result<(), ApiError> { /// 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. #[allow(clippy::unused_async)] -pub async fn show_handler(config: &Config, image: &str, tag: &str) -> Result<(), ApiError> { +pub async fn show_handler(registry_url: &Url, image: &str, tag: &str) -> Result<(), ApiError> { log::trace!("show_handler(image: {image}, tag: {tag})"); - let base = config.registry_url.clone(); let path = format!("/v2/{image}/manifests/{tag}"); - let _url = base.join(&path)?; + let _url = registry_url.join(&path)?; Ok(()) } @@ -105,7 +104,7 @@ pub async fn show_handler(config: &Config, image: &str, tag: &str) -> Result<(), /// manifest digest, or if there is a problem deleting the manifest from the /// Docker Registry API. #[allow(clippy::unused_async)] -pub async fn delete_handler(_config: &Config, image: &str, tag: &str) -> Result<(), ApiError> { +pub async fn delete_handler(_registry_url: &Url, image: &str, tag: &str) -> Result<(), ApiError> { log::trace!("delete_handler(image: {image}, tag: {tag})"); todo!() } @@ -118,12 +117,11 @@ pub async fn delete_handler(_config: &Config, image: &str, tag: &str) -> Result< /// /// 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> { +pub async fn check_handler(registry_url: &Url) -> Result<(), ApiError> { log::trace!("check_handler()"); - let base = config.registry_url.clone(); let path = "/v2"; - let url = base.join(path)?; + let url = registry_url.join(path)?; let response = reqwest::get(url).await?; diff --git a/src/error.rs b/src/error.rs index 897873a..36fc114 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,6 +28,10 @@ pub enum DredgeError { /// An error communicating with the Registry API #[error(transparent)] ApiError(#[from] ApiError), + + /// An error building the registry URL + #[error("Error determining registry URL from {0}")] + RegistryUrlError(String), } /// An error related to the configuration fo the program. diff --git a/src/main.rs b/src/main.rs index e28234e..be34ca9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,15 +16,12 @@ #![deny(clippy::pedantic)] -use std::ffi::OsString; -use std::path::PathBuf; - use clap::Parser; +use url::Url; use crate::cli::Cli; use crate::cli::Commands; -use crate::config::Config; -use crate::error::ConfigError; + use crate::error::DredgeError; mod api; @@ -33,55 +30,27 @@ mod commands; mod config; mod error; -/// The default basename of the main configuration file. -const CONFIG_FILE_NAME: &str = "dredge.toml"; - -/// The XDG directory prefix. -const CONFIG_PREFIX: &str = "dredge"; - -/// Locate the absolute path to the saved configuration file on disk. +/// Generate the full Docker Registry URL from a given `host[:port]` /// -/// If given an optional `path` to a configuration file, and that file -/// exists on disk, the absolute path to that file will be returned. -/// Otherwise, the XDG configuration path will be used. If neither the -/// optional `path` parameter refers to an existing file on disk, nor a -/// suitable configuration file can be located within the XDG configuration -/// path, the `None` variant will be returned. -fn locate_config_file(path: Option) -> Option { - log::trace!("locate_config_file({path:?})"); - - if let Some(path) = path { - let p = PathBuf::from(path); - log::debug!("Checking if path {p:?} exists"); - p.try_exists().map(|_| Some(p)).unwrap_or(None) - } else { - let xdg_dirs = xdg::BaseDirectories::with_prefix(CONFIG_PREFIX).ok()?; - let search_paths: Vec = vec![xdg_dirs.get_config_home()] - .into_iter() - .chain(xdg_dirs.get_config_dirs()) - .collect(); - log::debug!("Searching configuration directories for {CONFIG_FILE_NAME} {search_paths:?}"); - xdg_dirs.find_config_file(CONFIG_FILE_NAME) - } -} - -/// Attempt to create a default configuration file in the XDG configuration -/// path. Any sub-directories of the XDG configuration path which do not -/// already exist will be created automatically. +/// This prepends the HTTPS scheme and converts the given string to a `Url` +/// instance. +/// +/// If the given `host` value is already a valid URL, then it will be returned +/// as-is. /// /// # Errors: /// -/// This returns a `ConfigError` if a problem occurred which prevented either -/// the creation of the directory tree, or in writing the default configuration -/// to the file. -fn create_default_config_file() -> Result { - log::trace!("create_default_config_file()"); +/// If there is a problem parsing the resulting string as a valid URL, a +/// `DredgeError::RegistryUrlError` will be returned. +fn make_registry_url(host: &str) -> Result { + log::trace!("make_registry_url(host: {host})"); - let xdg_dirs = xdg::BaseDirectories::with_prefix(CONFIG_PREFIX)?; - let config_path = xdg_dirs.place_config_file(CONFIG_FILE_NAME)?; - let default_config = toml::to_string_pretty(&Config::default())?; - std::fs::write(&config_path, default_config)?; - Ok(config_path) + Url::parse(host) + .or_else(|_| { + let url_string = format!("https://{host}"); + Url::parse(&url_string) + }) + .or(Err(DredgeError::RegistryUrlError(host.to_string()))) } #[async_std::main] @@ -92,20 +61,20 @@ async fn main() -> Result<(), DredgeError> { let log_level = args.log_level; femme::with_level(log::LevelFilter::from(log_level)); - // -- Load and parse configuration file - let config_file = - locate_config_file(args.config).map_or_else(create_default_config_file, Ok)?; - log::debug!("Using configuration file {config_file:?}"); + // -- Generate the complete registry URL from the given host[:path] + let registry_url: Url = make_registry_url(&args.registry)?; - 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::Catalog => commands::catalog_handler(®istry_url).await?, + Commands::Tags { name } => commands::tags_handler(®istry_url, &name).await?, Commands::Show { image, tag } => { - commands::show_handler(&config, &image, &tag.unwrap_or("latest".to_string())).await?; + commands::show_handler(®istry_url, &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?, + Commands::Delete { image, tag } => { + commands::delete_handler(®istry_url, &image, &tag).await?; + } + Commands::Check => commands::check_handler(®istry_url).await?, } Ok(())