From 330b70752e6e38d354109f5135cf36b74c4e0e8b Mon Sep 17 00:00:00 2001 From: Sergio <77530549+sergi0g@users.noreply.github.com> Date: Sun, 15 Sep 2024 19:14:20 +0300 Subject: [PATCH] Improve logging --- .tool-versions | 1 - src/check.rs | 63 ++++++++++++++++++++------------- src/docker.rs | 26 ++++++++------ src/formatting.rs | 2 +- src/image.rs | 44 ++++++++++++++---------- src/main.rs | 33 ++++++++++++++---- src/registry.rs | 74 ++++++++++++++++++++++++++++----------- src/server.rs | 31 +++++++++-------- src/utils.rs | 86 +++++++++++++++++++++++++++++----------------- web/.tool-versions | 2 +- 10 files changed, 231 insertions(+), 131 deletions(-) delete mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index 6e0d11c..0000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -rust 1.79.0 diff --git a/src/check.rs b/src/check.rs index 585c647..aba3ab3 100644 --- a/src/check.rs +++ b/src/check.rs @@ -1,12 +1,14 @@ use std::collections::{HashMap, HashSet}; -use json::JsonValue; +use chrono::Local; use crate::{ + debug, docker::get_images_from_docker_daemon, image::Image, + info, registry::{check_auth, get_latest_digests, get_token}, - utils::{new_reqwest_client, unsplit_image}, + utils::{new_reqwest_client, unsplit_image, CliConfig}, }; #[cfg(feature = "cli")] @@ -29,44 +31,48 @@ where } } -pub async fn get_all_updates( - socket: Option, - config: &JsonValue, -) -> Vec<(String, Option)> { - let local_images = get_images_from_docker_daemon(socket).await; +pub async fn get_all_updates(options: &CliConfig) -> Vec<(String, Option)> { + let start = Local::now().timestamp_millis(); + let local_images = get_images_from_docker_daemon(options).await; let mut image_map: HashMap> = HashMap::with_capacity(local_images.len()); for image in &local_images { - let img = unsplit_image(&image.registry, &image.repository, &image.tag); + let img = unsplit_image(image); image_map.insert(img, image.digest.clone()); - }; - let mut registries: Vec<&String> = local_images - .iter() - .map(|image| &image.registry) - .collect(); + } + let mut registries: Vec<&String> = local_images.iter().map(|image| &image.registry).collect(); registries.unique(); let mut remote_images: Vec = Vec::with_capacity(local_images.len()); let client = new_reqwest_client(); for registry in registries { + if options.verbose { + debug!("Checking images from registry {}", registry) + } let images: Vec<&Image> = local_images .iter() .filter(|image| &image.registry == registry) .collect(); - let credentials = config["authentication"][registry] + let credentials = options.config["authentication"][registry] .clone() .take_string() .or(None); - let mut latest_images = match check_auth(registry, config, &client).await { + let mut latest_images = match check_auth(registry, options, &client).await { Some(auth_url) => { let token = get_token(images.clone(), &auth_url, &credentials, &client).await; - get_latest_digests(images, Some(&token), config, &client).await + if options.verbose { + debug!("Using token {}", token); + } + get_latest_digests(images, Some(&token), options, &client).await } - None => get_latest_digests(images, None, config, &client).await, + None => get_latest_digests(images, None, options, &client).await, }; remote_images.append(&mut latest_images); } + if options.verbose { + debug!("Collecting results") + } let mut result: Vec<(String, Option)> = Vec::new(); remote_images.iter().for_each(|image| { - let img = unsplit_image(&image.registry, &image.repository, &image.tag); + let img = unsplit_image(image); match &image.digest { Some(d) => { let r = d != image_map.get(&img).unwrap().as_ref().unwrap(); @@ -75,24 +81,33 @@ pub async fn get_all_updates( None => result.push((img, None)), } }); + let end = Local::now().timestamp_millis(); + info!( + "✨ Checked {} images in {}ms", + local_images.len(), + end - start + ); result } #[cfg(feature = "cli")] -pub async fn get_update(image: &str, socket: Option, config: &JsonValue) -> Option { - let local_image = get_image_from_docker_daemon(socket, image).await; - let credentials = config["authentication"][&local_image.registry] +pub async fn get_update(image: &str, options: &CliConfig) -> Option { + let local_image = get_image_from_docker_daemon(options.socket.clone(), image).await; + let credentials = options.config["authentication"][&local_image.registry] .clone() .take_string() .or(None); let client = new_reqwest_client(); - let token = match check_auth(&local_image.registry, config, &client).await { + let token = match check_auth(&local_image.registry, options, &client).await { Some(auth_url) => get_token(vec![&local_image], &auth_url, &credentials, &client).await, None => String::new(), }; + if options.verbose { + debug!("Using token {}", token); + }; let remote_image = match token.as_str() { - "" => get_latest_digest(&local_image, None, config, &client).await, - _ => get_latest_digest(&local_image, Some(&token), config, &client).await, + "" => get_latest_digest(&local_image, None, options, &client).await, + _ => get_latest_digest(&local_image, Some(&token), options, &client).await, }; match &remote_image.digest { Some(d) => Some(d != &local_image.digest.unwrap()), diff --git a/src/docker.rs b/src/docker.rs index 8ada41d..7e925b2 100644 --- a/src/docker.rs +++ b/src/docker.rs @@ -4,7 +4,11 @@ use bollard::{secret::ImageSummary, ClientVersion, Docker}; use bollard::secret::ImageInspect; use futures::future::join_all; -use crate::{error, image::Image, utils::split_image}; +use crate::{ + error, + image::Image, + utils::{split_image, CliConfig}, +}; fn create_docker_client(socket: Option) -> Docker { let client: Result = match socket { @@ -25,8 +29,8 @@ fn create_docker_client(socket: Option) -> Docker { } } -pub async fn get_images_from_docker_daemon(socket: Option) -> Vec { - let client: Docker = create_docker_client(socket); +pub async fn get_images_from_docker_daemon(options: &CliConfig) -> Vec { + let client: Docker = create_docker_client(options.socket.clone()); let images: Vec = match client.list_images::(None).await { Ok(images) => images, Err(e) => { @@ -35,14 +39,14 @@ pub async fn get_images_from_docker_daemon(socket: Option) -> Vec }; let mut handles = Vec::new(); for image in images { - handles.push(Image::from(image)) - }; - join_all(handles).await.iter().filter(|img| { - match img { - Some(_) => true, - None => false - } - }).map(|img| img.clone().unwrap()).collect() + handles.push(Image::from(image, options)) + } + join_all(handles) + .await + .iter() + .filter(|img| img.is_some()) + .map(|img| img.clone().unwrap()) + .collect() } #[cfg(feature = "cli")] diff --git a/src/formatting.rs b/src/formatting.rs index 07547bc..87f78f9 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -8,7 +8,7 @@ use crate::utils::{sort_update_vec, to_json}; pub fn print_updates(updates: &[(String, Option)], icons: &bool) { let sorted_updates = sort_update_vec(updates); let term_width: usize = termsize::get() - .unwrap_or_else(|| termsize::Size { rows: 24, cols: 80 }) + .unwrap_or(termsize::Size { rows: 24, cols: 80 }) .cols as usize; for update in sorted_updates { let description = match update.1 { diff --git a/src/image.rs b/src/image.rs index 933fcaa..c28a921 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,6 +1,9 @@ use bollard::secret::ImageSummary; -use crate::utils::split_image; +use crate::{ + debug, + utils::{split_image, CliConfig}, +}; #[derive(Clone, Debug)] pub struct Image { @@ -11,25 +14,28 @@ pub struct Image { } impl Image { - pub async fn from(image: ImageSummary) -> Option { + pub async fn from(image: ImageSummary, options: &CliConfig) -> Option { if !image.repo_tags.is_empty() && !image.repo_digests.is_empty() { - for t in &image.repo_tags { - let (registry, repository, tag) = split_image(t); - let image = Image { - registry, - repository, - tag, - digest: Some( - image.repo_digests[0] - .clone() - .split('@') - .collect::>()[1] - .to_string(), - ), - }; - return Some(image) - } + let (registry, repository, tag) = split_image(&image.repo_tags[0]); + let image = Image { + registry, + repository, + tag, + digest: Some( + image.repo_digests[0] + .clone() + .split('@') + .collect::>()[1] + .to_string(), + ), + }; + return Some(image); + } else if options.verbose { + debug!( + "Skipped an image\nTags: {:#?}\nDigests: {:#?}", + image.repo_tags, image.repo_digests + ) } None } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 185572a..bac14e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use formatting::{print_raw_update, print_raw_updates, print_update, print_update #[cfg(feature = "server")] use server::serve; use std::path::PathBuf; -use utils::load_config; +use utils::{load_config, CliConfig}; pub mod check; pub mod docker; @@ -25,6 +25,13 @@ struct Cli { socket: Option, #[arg(short, long, default_value_t = String::new(), help = "Config file path")] config_path: String, + #[arg( + short, + long, + default_value_t = false, + help = "Enable verbose (debug) logging" + )] + verbose: bool, #[command(subcommand)] command: Option, } @@ -64,23 +71,35 @@ async fn main() { "" => None, path => Some(PathBuf::from(path)), }; - let config = load_config(cfg_path); + if cli.verbose { + debug!("CLI options:"); + debug!("Config path: {:?}", cfg_path); + debug!("Socket: {:?}", &cli.socket) + } + let cli_config = CliConfig { + socket: cli.socket, + verbose: cli.verbose, + config: load_config(cfg_path), + }; + if cli.verbose { + debug!("Config: {}", cli_config.config) + } match &cli.command { #[cfg(feature = "cli")] Some(Commands::Check { image, icons, raw }) => match image { Some(name) => { - let has_update = get_update(name, cli.socket, &config).await; + let has_update = get_update(name, &cli_config).await; match raw { true => print_raw_update(name, &has_update), false => print_update(name, &has_update), }; } None => { - match raw { - true => print_raw_updates(&get_all_updates(cli.socket, &config).await), + match *raw || cli.verbose { + true => print_raw_updates(&get_all_updates(&cli_config).await), false => { let spinner = Spinner::new(); - let updates = get_all_updates(cli.socket, &config).await; + let updates = get_all_updates(&cli_config).await; spinner.succeed(); print_updates(&updates, icons); } @@ -89,7 +108,7 @@ async fn main() { }, #[cfg(feature = "server")] Some(Commands::Serve { port }) => { - let _ = serve(port, cli.socket, config).await; + let _ = serve(port, &cli_config).await; } None => (), } diff --git a/src/registry.rs b/src/registry.rs index 9270665..643f165 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -4,15 +4,28 @@ use json::JsonValue; use http_auth::parse_challenges; use reqwest_middleware::ClientWithMiddleware; -use crate::{error, image::Image, warn}; +use crate::{debug, error, image::Image, utils::CliConfig, warn}; -pub async fn check_auth(registry: &str, config: &JsonValue, client: &ClientWithMiddleware) -> Option { - let protocol = if config["insecure_registries"].contains(registry) { +pub async fn check_auth( + registry: &str, + options: &CliConfig, + client: &ClientWithMiddleware, +) -> Option { + let protocol = if options.config["insecure_registries"].contains(registry) { + if options.verbose { + debug!( + "{} is configured as an insecure registry. Downgrading to HTTP", + registry + ); + }; "http" } else { "https" }; - let response = client.get(&format!("{}://{}/v2/", protocol, registry)).send().await; + let response = client + .get(format!("{}://{}/v2/", protocol, registry)) + .send() + .await; match response { Ok(r) => { let status = r.status().as_u16(); @@ -27,10 +40,14 @@ pub async fn check_auth(registry: &str, config: &JsonValue, client: &ClientWithM } else if status == 200 { None } else { - warn!("Received unexpected status code {}\nResponse: {}", status, r.text().await.unwrap()); + warn!( + "Received unexpected status code {}\nResponse: {}", + status, + r.text().await.unwrap() + ); None } - }, + } Err(e) => { if e.is_connect() { warn!("Connection to registry {} failed.", ®istry); @@ -42,14 +59,20 @@ pub async fn check_auth(registry: &str, config: &JsonValue, client: &ClientWithM } } -pub async fn get_latest_digest(image: &Image, token: Option<&String>, config: &JsonValue, client: &ClientWithMiddleware) -> Image { - let protocol = - if config["insecure_registries"].contains(json::JsonValue::from(image.registry.clone())) { - "http" - } else { - "https" - }; - let mut request = client.head(&format!( +pub async fn get_latest_digest( + image: &Image, + token: Option<&String>, + options: &CliConfig, + client: &ClientWithMiddleware, +) -> Image { + let protocol = if options.config["insecure_registries"] + .contains(json::JsonValue::from(image.registry.clone())) + { + "http" + } else { + "https" + }; + let mut request = client.head(format!( "{}://{}/v2/{}/manifests/{}", protocol, &image.registry, &image.repository, &image.tag )); @@ -90,30 +113,39 @@ pub async fn get_latest_digest(image: &Image, token: Option<&String>, config: &J digest: Some(digest.to_str().unwrap().to_string()), ..image.clone() }, - None => error!("Server returned invalid response! No docker-content-digest!\n{:#?}", raw_response), + None => error!( + "Server returned invalid response! No docker-content-digest!\n{:#?}", + raw_response + ), } } pub async fn get_latest_digests( images: Vec<&Image>, token: Option<&String>, - config: &JsonValue, - client: &ClientWithMiddleware + options: &CliConfig, + client: &ClientWithMiddleware, ) -> Vec { let mut handles = Vec::new(); for image in images { - handles.push(get_latest_digest(image, token, config, client)) + handles.push(get_latest_digest(image, token, options, client)) } join_all(handles).await } -pub async fn get_token(images: Vec<&Image>, auth_url: &str, credentials: &Option, client: &ClientWithMiddleware) -> String { +pub async fn get_token( + images: Vec<&Image>, + auth_url: &str, + credentials: &Option, + client: &ClientWithMiddleware, +) -> String { let mut final_url = auth_url.to_owned(); for image in &images { final_url = format!("{}&scope=repository:{}:pull", final_url, image.repository); } - let mut base_request = - client.get(&final_url).header("Accept", "application/vnd.oci.image.index.v1+json"); // Seems to be unnecessary. Will probably remove in the future + let mut base_request = client + .get(&final_url) + .header("Accept", "application/vnd.oci.image.index.v1+json"); // Seems to be unnecessary. Will probably remove in the future base_request = match credentials { Some(creds) => base_request.header("Authorization", &format!("Basic {}", creds)), None => base_request, diff --git a/src/server.rs b/src/server.rs index c4fcc90..ce1d5c8 100644 --- a/src/server.rs +++ b/src/server.rs @@ -15,8 +15,8 @@ use xitca_web::{ use crate::{ check::get_all_updates, - error, - utils::{sort_update_vec, to_json}, + error, info, + utils::{sort_update_vec, to_json, CliConfig}, }; const HTML: &str = include_str!("static/index.html"); @@ -26,9 +26,10 @@ const FAVICON_ICO: &[u8] = include_bytes!("static/favicon.ico"); const FAVICON_SVG: &[u8] = include_bytes!("static/favicon.svg"); const APPLE_TOUCH_ICON: &[u8] = include_bytes!("static/apple-touch-icon.png"); -pub async fn serve(port: &u16, socket: Option, config: JsonValue) -> std::io::Result<()> { - println!("Starting server, please wait..."); - let data = ServerData::new(socket, config).await; +pub async fn serve(port: &u16, options: &CliConfig) -> std::io::Result<()> { + info!("Starting server, please wait..."); + let data = ServerData::new(options).await; + info!("Ready to start!"); App::new() .with_state(Arc::new(Mutex::new(data))) .at("/", get(handler_service(_static))) @@ -93,31 +94,28 @@ struct ServerData { template: String, raw_updates: Vec<(String, Option)>, json: JsonValue, - socket: Option, - config: JsonValue, + options: CliConfig, theme: &'static str, } impl ServerData { - async fn new(socket: Option, config: JsonValue) -> Self { + async fn new(options: &CliConfig) -> Self { let mut s = Self { - socket, + options: options.clone(), template: String::new(), json: json::object! { metrics: json::object! {}, images: json::object! {}, }, raw_updates: Vec::new(), - config, theme: "neutral", }; s.refresh().await; s } async fn refresh(&mut self) { - let updates = sort_update_vec( - &get_all_updates(self.socket.clone(), &self.config["authentication"]).await, - ); + info!("Refreshing data"); + let updates = sort_update_vec(&get_all_updates(&self.options).await); self.raw_updates = updates; let template = liquid::ParserBuilder::with_stdlib() .build() @@ -134,8 +132,11 @@ impl ServerData { .collect::>(); self.json = to_json(&self.raw_updates); let last_updated = Local::now(); - self.json["last_updated"] = last_updated.to_rfc3339_opts(chrono::SecondsFormat::Secs, true).to_string().into(); - self.theme = match &self.config["theme"].as_str() { + self.json["last_updated"] = last_updated + .to_rfc3339_opts(chrono::SecondsFormat::Secs, true) + .to_string() + .into(); + self.theme = match &self.options.config["theme"].as_str() { Some(t) => match *t { "default" => "neutral", "blue" => "gray", diff --git a/src/utils.rs b/src/utils.rs index b5ade7a..f34ff6b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,27 +1,11 @@ use std::path::PathBuf; +use crate::{error, image::Image}; use json::{object, JsonValue}; use once_cell::sync::Lazy; use regex::Regex; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; -use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff}; - -/// 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!($($arg)*); - std::process::exit(1); - }) -} - -// A small macro to print in yellow as a warning -#[macro_export] -macro_rules! warn { - ($($arg:tt)*) => ({ - eprintln!("\x1b[93m{}\x1b[0m", format!($($arg)*)); - }) -} +use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; static RE: Lazy = Lazy::new(|| { Regex::new( @@ -62,22 +46,22 @@ pub fn split_image(image: &str) -> (String, String, String) { } /// Given an image's parts which were previously created by split_image, recreate a reference that docker would use. This means removing the registry part, if it's Docker Hub and removing "library" if the image is official -pub fn unsplit_image(registry: &str, repository: &str, tag: &str) -> String { - let reg = match registry { +pub fn unsplit_image(image: &Image) -> String { + let reg = match image.registry.as_str() { "registry-1.docker.io" => String::new(), r => format!("{}/", r), }; - let repo = match repository.split('/').collect::>()[0] { + let repo = match image.repository.split('/').collect::>()[0] { "library" => { if reg.is_empty() { - repository.strip_prefix("library/").unwrap() + image.repository.strip_prefix("library/").unwrap() } else { - repository + image.repository.as_str() } } - _ => repository, + _ => image.repository.as_str(), }; - format!("{}{}:{}", reg, repo, tag) + format!("{}{}:{}", reg, repo, image.tag) } /// Sorts the update vector alphabetically and where Some(true) > Some(false) > None @@ -132,10 +116,7 @@ pub fn to_json(updates: &[(String, Option)]) -> JsonValue { .iter() .filter(|&(_, value)| *value == Some(true)) .count(); - let unknown = updates - .iter() - .filter(|&(_, value)| value.is_none()) - .count(); + let unknown = updates.iter().filter(|&(_, value)| value.is_none()).count(); let _ = json_data["metrics"].insert("monitored_images", updates.len()); let _ = json_data["metrics"].insert("up_to_date", up_to_date); let _ = json_data["metrics"].insert("update_available", update_available); @@ -143,8 +124,51 @@ pub fn to_json(updates: &[(String, Option)]) -> JsonValue { json_data } +/// Struct to hold some config values to avoid having to pass them all the time +#[derive(Clone)] +pub struct CliConfig { + pub socket: Option, + pub verbose: bool, + pub config: JsonValue, +} + +// Logging + +/// 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[41m ERROR \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[103m WARN \x1b[0m {}", format!($($arg)*)); + }) +} + +#[macro_export] +macro_rules! info { + ($($arg:tt)*) => ({ + println!("\x1b[44m INFO \x1b[0m {}", format!($($arg)*)); + }) +} + +#[macro_export] +macro_rules! debug { + ($($arg:tt)*) => ({ + println!("\x1b[48:5:57m DEBUG \x1b[0m {}", format!($($arg)*)); + }) +} + pub fn new_reqwest_client() -> ClientWithMiddleware { ClientBuilder::new(reqwest::Client::new()) - .with(RetryTransientMiddleware::new_with_policy(ExponentialBackoff::builder().build_with_max_retries(3))) + .with(RetryTransientMiddleware::new_with_policy( + ExponentialBackoff::builder().build_with_max_retries(3), + )) .build() -} \ No newline at end of file +} diff --git a/web/.tool-versions b/web/.tool-versions index cc1993f..b8c6605 100644 --- a/web/.tool-versions +++ b/web/.tool-versions @@ -1 +1 @@ -nodejs 21.6.2 +nodejs 22.8.0