diff --git a/src/check.rs b/src/check.rs index 40b18d1..2e4cb4e 100644 --- a/src/check.rs +++ b/src/check.rs @@ -3,21 +3,19 @@ use itertools::Itertools; use rustc_hash::{FxHashMap, FxHashSet}; use crate::{ - config::Config, - debug, docker::get_images_from_docker_daemon, http::Client, registry::{check_auth, get_token}, structs::{image::Image, update::Update}, utils::request::{get_response_body, parse_json}, - warn, + Context, }; /// Fetches image data from other Cup instances -async fn get_remote_updates(servers: &FxHashMap, client: &Client) -> Vec { +async fn get_remote_updates(ctx: &Context, client: &Client) -> Vec { let mut remote_images = Vec::new(); - let handles: Vec<_> = servers + let handles: Vec<_> = ctx.config.servers .iter() .map(|(name, url)| async { let url = if url.starts_with("http://") || url.starts_with("https://") { @@ -28,7 +26,7 @@ async fn get_remote_updates(servers: &FxHashMap, client: &Client match client.get(&url, vec![], false).await { Ok(response) => { if response.status() != 200 { - warn!("GET {}: Failed to fetch updates from server. Server returned invalid response code: {}",url,response.status()); + ctx.logger.warn(format!("GET {}: Failed to fetch updates from server. Server returned invalid response code: {}",url,response.status())); return Vec::new(); } let json = parse_json(&get_response_body(response).await); @@ -48,7 +46,7 @@ async fn get_remote_updates(servers: &FxHashMap, client: &Client Vec::new() } Err(e) => { - warn!("Failed to fetch updates from server. {}", e); + ctx.logger.warn(format!("Failed to fetch updates from server. {}", e)); Vec::new() }, } @@ -63,12 +61,12 @@ async fn get_remote_updates(servers: &FxHashMap, client: &Client } /// Returns a list of updates for all images passed in. -pub async fn get_updates(references: &Option>, config: &Config) -> Vec { - let client = Client::new(); +pub async fn get_updates(references: &Option>, ctx: &Context) -> Vec { + let client = Client::new(ctx); // Get local images - debug!(config.debug, "Retrieving images to be checked"); - let mut images = get_images_from_docker_daemon(config, references).await; + ctx.logger.debug("Retrieving images to be checked"); + let mut images = get_images_from_docker_daemon(ctx, references).await; // Add extra images from references if let Some(refs) = references { @@ -82,18 +80,17 @@ pub async fn get_updates(references: &Option>, config: &Config) -> V } // Get remote images from other servers - let remote_updates = if !config.servers.is_empty() { - debug!(config.debug, "Fetching updates from remote servers"); - get_remote_updates(&config.servers, &client).await + let remote_updates = if !ctx.config.servers.is_empty() { + ctx.logger.debug("Fetching updates from remote servers"); + get_remote_updates(ctx, &client).await } else { Vec::new() }; - debug!( - config.debug, + ctx.logger.debug(format!( "Checking {:?}", images.iter().map(|image| &image.reference).collect_vec() - ); + )); // Get a list of unique registries our images belong to. We are unwrapping the registry because it's guaranteed to be there. let registries: Vec<&String> = images @@ -104,7 +101,7 @@ pub async fn get_updates(references: &Option>, config: &Config) -> V // Create request client. All network requests share the same client for better performance. // This client is also configured to retry a failed request up to 3 times with exponential backoff in between. - let client = Client::new(); + let client = Client::new(ctx); // Create a map of images indexed by registry. This solution seems quite inefficient, since each iteration causes a key to be looked up. I can't find anything better at the moment. let mut image_map: FxHashMap<&String, Vec<&Image>> = FxHashMap::default(); @@ -119,12 +116,12 @@ pub async fn get_updates(references: &Option>, config: &Config) -> V // Retrieve an authentication token (if required) for each registry. let mut tokens: FxHashMap<&str, Option> = FxHashMap::default(); for registry in registries { - let credentials = if let Some(registry_config) = config.registries.get(registry) { + let credentials = if let Some(registry_config) = ctx.config.registries.get(registry) { ®istry_config.authentication } else { &None }; - match check_auth(registry, config, &client).await { + match check_auth(registry, ctx, &client).await { Some(auth_url) => { let token = get_token( image_map.get(registry).unwrap(), @@ -141,9 +138,10 @@ pub async fn get_updates(references: &Option>, config: &Config) -> V } } - debug!(config.debug, "Tokens: {:?}", tokens); + ctx.logger.debug(format!("Tokens: {:?}", tokens)); - let ignored_registries = config + let ignored_registries = ctx + .config .registries .iter() .filter_map(|(registry, registry_config)| { @@ -160,14 +158,15 @@ pub async fn get_updates(references: &Option>, config: &Config) -> V // Loop through images check for updates for image in &images { let is_ignored = ignored_registries.contains(&&image.parts.registry) - || config + || ctx + .config .images .exclude .iter() .any(|item| image.reference.starts_with(item)); if !is_ignored { let token = tokens.get(image.parts.registry.as_str()).unwrap(); - let future = image.check(token.as_deref(), config, &client); + let future = image.check(token.as_deref(), ctx, &client); handles.push(future); } } diff --git a/src/config.rs b/src/config.rs index cbc3d06..2e0c20a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -40,8 +40,6 @@ pub struct ImageConfig { pub struct Config { version: u8, pub agent: bool, - #[serde(skip_deserializing)] - pub debug: bool, pub images: ImageConfig, pub refresh_interval: Option, pub registries: FxHashMap, @@ -55,7 +53,6 @@ impl Config { Self { version: 3, agent: false, - debug: false, images: ImageConfig::default(), refresh_interval: None, registries: FxHashMap::default(), diff --git a/src/docker.rs b/src/docker.rs index b197629..e7e8db7 100644 --- a/src/docker.rs +++ b/src/docker.rs @@ -2,7 +2,7 @@ use bollard::{models::ImageInspect, ClientVersion, Docker}; use futures::future::join_all; -use crate::{config::Config, error, structs::image::Image}; +use crate::{error, structs::image::Image, Context}; fn create_docker_client(socket: Option<&str>) -> Docker { let client: Result = match socket { @@ -38,10 +38,10 @@ fn create_docker_client(socket: Option<&str>) -> Docker { /// Retrieves images from Docker daemon. If `references` is Some, return only the images whose references match the ones specified. pub async fn get_images_from_docker_daemon( - config: &Config, + ctx: &Context, references: &Option>, ) -> Vec { - let client: Docker = create_docker_client(config.socket.as_deref()); + let client: Docker = create_docker_client(ctx.config.socket.as_deref()); match references { Some(refs) => { let mut inspect_handles = Vec::with_capacity(refs.len()); diff --git a/src/http.rs b/src/http.rs index eecb848..71a3996 100644 --- a/src/http.rs +++ b/src/http.rs @@ -4,7 +4,7 @@ use reqwest::Response; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; -use crate::{error, warn}; +use crate::{error, Context}; pub enum RequestMethod { GET, @@ -23,16 +23,18 @@ impl Display for RequestMethod { /// A struct for handling HTTP requests. Takes care of the repetitive work of checking for errors, etc and exposes a simple interface pub struct Client { inner: ClientWithMiddleware, + ctx: Context, } impl Client { - pub fn new() -> Self { + pub fn new(ctx: &Context) -> Self { Self { inner: ClientBuilder::new(reqwest::Client::new()) .with(RetryTransientMiddleware::new_with_policy( ExponentialBackoff::builder().build_with_max_retries(3), )) .build(), + ctx: ctx.clone(), } } @@ -57,14 +59,14 @@ impl Client { let status = response.status(); if status == 404 { let message = format!("{} {}: Not found!", method, url); - warn!("{}", message); + self.ctx.logger.warn(&message); Err(message) } else if status == 401 { if ignore_401 { Ok(response) } else { let message = format!("{} {}: Unauthorized! Please configure authentication for this registry or if you have already done so, please make sure it is correct.", method, url); - warn!("{}", message); + self.ctx.logger.warn(&message); Err(message) } } else if status.as_u16() <= 400 { @@ -87,11 +89,11 @@ impl Client { Err(error) => { if error.is_connect() { let message = format!("{} {}: Connection failed!", method, url); - warn!("{}", message); + self.ctx.logger.warn(&message); Err(message) } else if error.is_timeout() { let message = format!("{} {}: Connection timed out!", method, url); - warn!("{}", message); + self.ctx.logger.warn(&message); Err(message) } else { error!( @@ -123,9 +125,3 @@ impl Client { self.request(url, RequestMethod::HEAD, headers, false).await } } - -impl Default for Client { - fn default() -> Self { - Self::new() - } -} diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..dd7b169 --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,42 @@ +#[macro_export] +macro_rules! error { + ($($arg:tt)*) => ({ + eprintln!("\x1b[31;1mERROR\x1b[0m {}", format!($($arg)*)); + std::process::exit(1); + }) +} + +/// This struct mostly exists so we can print stuff without passing debug or raw every time. +#[derive(Clone)] +pub struct Logger { + debug: bool, + raw: bool, +} + +impl Logger { + pub fn new(debug: bool, raw: bool) -> Self { + Self { debug, raw } + } + + pub fn warn(&self, msg: impl AsRef) { + if !self.raw { + eprintln!("\x1b[33;1m WARN\x1b[0m {}", msg.as_ref()); + } + } + + pub fn info(&self, msg: impl AsRef) { + if !self.raw { + println!("\x1b[36;1m INFO\x1b[0m {}", msg.as_ref()); + } + } + + pub fn debug(&self, msg: impl AsRef) { + if self.debug { + println!("\x1b[35;1mDEBUG\x1b[0m {}", msg.as_ref()); + } + } + + pub fn set_raw(&mut self, raw: bool) { + self.raw = raw + } +} diff --git a/src/main.rs b/src/main.rs index b67c663..f79453a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use config::Config; use formatting::spinner::Spinner; #[cfg(feature = "cli")] use formatting::{print_raw_updates, print_updates}; +use logging::Logger; #[cfg(feature = "server")] use server::serve; use std::path::PathBuf; @@ -15,6 +16,7 @@ pub mod docker; #[cfg(feature = "cli")] pub mod formatting; pub mod http; +pub mod logging; pub mod registry; #[cfg(feature = "server")] pub mod server; @@ -62,6 +64,12 @@ enum Commands { }, } +#[derive(Clone)] +pub struct Context { + pub config: Config, + pub logger: Logger, +} + #[tokio::main] async fn main() { let cli = Cli::parse(); @@ -73,7 +81,10 @@ async fn main() { if let Some(socket) = cli.socket { config.socket = Some(socket) } - config.debug = cli.debug; + let mut ctx = Context { + config, + logger: Logger::new(cli.debug, false), + }; match &cli.command { #[cfg(feature = "cli")] Some(Commands::Check { @@ -82,23 +93,26 @@ async fn main() { raw, }) => { let start = SystemTime::now(); - match *raw || config.debug { + if *raw { + ctx.logger.set_raw(true); + } + match *raw || cli.debug { true => { - let updates = get_updates(references, &config).await; + let updates = get_updates(references, &ctx).await; print_raw_updates(&updates); } false => { let spinner = Spinner::new(); - let updates = get_updates(references, &config).await; + let updates = get_updates(references, &ctx).await; spinner.succeed(); print_updates(&updates, icons); - info!("✨ Checked {} images in {}ms", updates.len(), start.elapsed().unwrap().as_millis()); + ctx.logger.info(format!("✨ Checked {} images in {}ms", updates.len(), start.elapsed().unwrap().as_millis())); } }; } #[cfg(feature = "server")] Some(Commands::Serve { port }) => { - let _ = serve(port, &config).await; + let _ = serve(port, &ctx).await; } None => error!("Whoops! It looks like you haven't specified a command to run! Try `cup help` to see available options."), } diff --git a/src/registry.rs b/src/registry.rs index c655602..a5bcb5c 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -3,8 +3,7 @@ use std::time::SystemTime; use itertools::Itertools; use crate::{ - config::Config, - debug, error, + error, http::Client, structs::{ image::{DigestInfo, Image, VersionInfo}, @@ -17,10 +16,11 @@ use crate::{ }, time::{elapsed, now}, }, + Context, }; -pub async fn check_auth(registry: &str, config: &Config, client: &Client) -> Option { - let protocol = get_protocol(registry, &config.registries); +pub async fn check_auth(registry: &str, ctx: &Context, client: &Client) -> Option { + let protocol = get_protocol(registry, &ctx.config.registries); let url = format!("{}://{}/v2/", protocol, registry); let response = client.get(&url, Vec::new(), true).await; match response { @@ -45,15 +45,13 @@ pub async fn check_auth(registry: &str, config: &Config, client: &Client) -> Opt pub async fn get_latest_digest( image: &Image, token: Option<&str>, - config: &Config, + ctx: &Context, client: &Client, ) -> Image { - debug!( - config.debug, - "Checking for digest update to {}", image.reference - ); + ctx.logger + .debug(format!("Checking for digest update to {}", image.reference)); let start = SystemTime::now(); - let protocol = get_protocol(&image.parts.registry, &config.registries); + let protocol = get_protocol(&image.parts.registry, &ctx.config.registries); let url = format!( "{}://{}/v2/{}/manifests/{}", protocol, &image.parts.registry, &image.parts.repository, &image.parts.tag @@ -63,10 +61,10 @@ pub async fn get_latest_digest( let response = client.head(&url, headers).await; let time = start.elapsed().unwrap().as_millis() as u32; - debug!( - config.debug, - "Checked for digest update to {} in {}ms", image.reference, time - ); + ctx.logger.debug(format!( + "Checked for digest update to {} in {}ms", + image.reference, time + )); match response { Ok(res) => match res.headers().get("docker-content-digest") { Some(digest) => { @@ -121,15 +119,13 @@ pub async fn get_latest_tag( image: &Image, base: &Version, token: Option<&str>, - config: &Config, + ctx: &Context, client: &Client, ) -> Image { - debug!( - config.debug, - "Checking for tag update to {}", image.reference - ); + ctx.logger + .debug(format!("Checking for tag update to {}", image.reference)); let start = now(); - let protocol = get_protocol(&image.parts.registry, &config.registries); + let protocol = get_protocol(&image.parts.registry, &ctx.config.registries); let url = format!( "{}://{}/v2/{}/tags/list", protocol, &image.parts.registry, &image.parts.repository, @@ -144,12 +140,11 @@ pub async fn get_latest_tag( let mut next_url = Some(url); while next_url.is_some() { - debug!( - config.debug, + ctx.logger.debug(format!( "{} has extra tags! Current number of valid tags: {}", image.reference, tags.len() - ); + )); let (new_tags, next) = match get_extra_tags( &next_url.unwrap(), headers.clone(), @@ -172,20 +167,19 @@ pub async fn get_latest_tag( next_url = next; } let tag = tags.iter().max(); - debug!( - config.debug, + ctx.logger.debug(format!( "Checked for tag update to {} in {}ms", image.reference, elapsed(start) - ); + )); match tag { Some(t) => { if t == base && image.digest_info.is_some() { // Tags are equal so we'll compare digests - debug!( - config.debug, - "Tags for {} are equal, comparing digests.", image.reference - ); + ctx.logger.debug(format!( + "Tags for {} are equal, comparing digests.", + image.reference + )); get_latest_digest( &Image { version_info: Some(VersionInfo { @@ -196,7 +190,7 @@ pub async fn get_latest_tag( ..image.clone() }, token, - config, + ctx, client, ) .await diff --git a/src/server.rs b/src/server.rs index 6d648e6..6ea44b9 100644 --- a/src/server.rs +++ b/src/server.rs @@ -18,14 +18,14 @@ use xitca_web::{ use crate::{ check::get_updates, - config::{Config, Theme}, - info, + config::Theme, structs::update::Update, utils::{ json::{to_full_json, to_simple_json}, sort_update_vec::sort_update_vec, time::{elapsed, now}, }, + Context, }; const HTML: &str = include_str!("static/index.html"); @@ -46,13 +46,13 @@ const SORT_ORDER: [&str; 8] = [ "unknown", ]; // For Liquid rendering -pub async fn serve(port: &u16, config: &Config) -> std::io::Result<()> { - info!("Starting server, please wait..."); - let data = ServerData::new(config).await; +pub async fn serve(port: &u16, ctx: &Context) -> std::io::Result<()> { + ctx.logger.info("Starting server, please wait..."); + let data = ServerData::new(ctx).await; let scheduler = JobScheduler::new().await.unwrap(); let data = Arc::new(Mutex::new(data)); let data_copy = data.clone(); - if let Some(interval) = &config.refresh_interval { + if let Some(interval) = &ctx.config.refresh_interval { scheduler .add( Job::new_async(interval, move |_uuid, _lock| { @@ -67,14 +67,14 @@ pub async fn serve(port: &u16, config: &Config) -> std::io::Result<()> { .unwrap(); } scheduler.start().await.unwrap(); - info!("Ready to start!"); + ctx.logger.info("Ready to start!"); let mut app_builder = App::new() .with_state(data) .at("/api/v2/json", get(handler_service(api_simple))) .at("/api/v3/json", get(handler_service(api_full))) .at("/api/v2/refresh", get(handler_service(refresh))) .at("/api/v3/refresh", get(handler_service(refresh))); - if !config.agent { + if !ctx.config.agent { app_builder = app_builder .at("/", get(handler_service(_static))) .at("/*", get(handler_service(_static))); @@ -151,14 +151,14 @@ struct ServerData { raw_updates: Vec, simple_json: Value, full_json: Value, - config: Config, + ctx: Context, theme: &'static str, } impl ServerData { - async fn new(config: &Config) -> Self { + async fn new(ctx: &Context) -> Self { let mut s = Self { - config: config.clone(), + ctx: ctx.clone(), template: String::new(), simple_json: Value::Null, full_json: Value::Null, @@ -171,14 +171,14 @@ impl ServerData { async fn refresh(&mut self) { let start = now(); if !self.raw_updates.is_empty() { - info!("Refreshing data"); + self.ctx.logger.info("Refreshing data"); } - let updates = sort_update_vec(&get_updates(&None, &self.config).await); - info!( + let updates = sort_update_vec(&get_updates(&None, &self.ctx).await); + self.ctx.logger.info(format!( "✨ Checked {} images in {}ms", updates.len(), elapsed(start) - ); + )); self.raw_updates = updates; let template = liquid::ParserBuilder::with_stdlib() .build() @@ -193,7 +193,7 @@ impl ServerData { .to_string() .into(); self.full_json["last_updated"] = self.simple_json["last_updated"].clone(); - self.theme = match &self.config.theme { + self.theme = match &self.ctx.config.theme { Theme::Default => "neutral", Theme::Blue => "gray", }; diff --git a/src/structs/image.rs b/src/structs/image.rs index 58fda61..4c36309 100644 --- a/src/structs/image.rs +++ b/src/structs/image.rs @@ -1,10 +1,10 @@ use crate::{ - config::Config, error, http::Client, registry::{get_latest_digest, get_latest_tag}, structs::{status::Status, version::Version}, utils::reference::split, + Context, }; use super::{ @@ -168,8 +168,20 @@ impl Image { .replacen("{}", &new_tag.minor.unwrap_or(0).to_string(), 1) .replacen("{}", &new_tag.patch.unwrap_or(0).to_string(), 1), // Throwing these in, because they're useful for the CLI output, however we won't (de)serialize them - current_version: self.version_info.as_ref().unwrap().current_tag.to_string(), - new_version: self.version_info.as_ref().unwrap().latest_remote_tag.as_ref().unwrap().to_string() + current_version: self + .version_info + .as_ref() + .unwrap() + .current_tag + .to_string(), + new_version: self + .version_info + .as_ref() + .unwrap() + .latest_remote_tag + .as_ref() + .unwrap() + .to_string(), }) } "digest" => { @@ -185,7 +197,7 @@ impl Image { }) } "none" => UpdateInfo::None, - _ => unreachable!() + _ => unreachable!(), }, }, error: self.error.clone(), @@ -197,11 +209,11 @@ impl Image { } /// Checks if the image has an update - pub async fn check(&self, token: Option<&str>, config: &Config, client: &Client) -> Self { + pub async fn check(&self, token: Option<&str>, ctx: &Context, client: &Client) -> Self { match &self.version_info { - Some(data) => get_latest_tag(self, &data.current_tag, token, config, client).await, + Some(data) => get_latest_tag(self, &data.current_tag, token, ctx, client).await, None => match self.digest_info { - Some(_) => get_latest_digest(self, token, config, client).await, + Some(_) => get_latest_digest(self, token, ctx, client).await, None => unreachable!(), }, } diff --git a/src/utils/logging.rs b/src/utils/logging.rs deleted file mode 100644 index 5328227..0000000 --- a/src/utils/logging.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Logging utilites - -/// This macro is an alternative to panic. It prints the message you give it and exits the process with code 1, without printing a stack trace. Useful for when the program has to exit due to a user error or something unexpected which is unrelated to the program (e.g. a failed web request) -#[macro_export] -macro_rules! error { - ($($arg:tt)*) => ({ - eprintln!("\x1b[31;1mERROR\x1b[0m {}", format!($($arg)*)); - std::process::exit(1); - }) -} - -// A small macro to print in yellow as a warning -#[macro_export] -macro_rules! warn { - ($($arg:tt)*) => ({ - eprintln!("\x1b[33;1mWARN \x1b[0m {}", format!($($arg)*)); - }) -} - -#[macro_export] -macro_rules! info { - ($($arg:tt)*) => ({ - println!("\x1b[36;1mINFO \x1b[0m {}", format!($($arg)*)); - }) -} - -#[macro_export] -macro_rules! debug { - ($debg:expr, $($arg:tt)*) => ({ - if $debg { - println!("\x1b[35;1mDEBUG\x1b[0m {}", format!($($arg)*)); - } - }) -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 8aa3de6..89263bb 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,5 @@ pub mod json; pub mod link; -pub mod logging; pub mod reference; pub mod request; pub mod sort_update_vec;