m/cup
1
0
mirror of https://github.com/sergi0g/cup.git synced 2025-11-14 16:13:48 -05:00

Improve logging

This commit is contained in:
Sergio
2024-09-15 19:14:20 +03:00
committed by GitHub
parent 0c9ad61a4d
commit 330b70752e
10 changed files with 231 additions and 131 deletions

View File

@@ -1 +0,0 @@
rust 1.79.0

View File

@@ -1,12 +1,14 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use json::JsonValue; use chrono::Local;
use crate::{ use crate::{
debug,
docker::get_images_from_docker_daemon, docker::get_images_from_docker_daemon,
image::Image, image::Image,
info,
registry::{check_auth, get_latest_digests, get_token}, registry::{check_auth, get_latest_digests, get_token},
utils::{new_reqwest_client, unsplit_image}, utils::{new_reqwest_client, unsplit_image, CliConfig},
}; };
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
@@ -29,44 +31,48 @@ where
} }
} }
pub async fn get_all_updates( pub async fn get_all_updates(options: &CliConfig) -> Vec<(String, Option<bool>)> {
socket: Option<String>, let start = Local::now().timestamp_millis();
config: &JsonValue, let local_images = get_images_from_docker_daemon(options).await;
) -> Vec<(String, Option<bool>)> {
let local_images = get_images_from_docker_daemon(socket).await;
let mut image_map: HashMap<String, Option<String>> = HashMap::with_capacity(local_images.len()); let mut image_map: HashMap<String, Option<String>> = HashMap::with_capacity(local_images.len());
for image in &local_images { 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()); image_map.insert(img, image.digest.clone());
}; }
let mut registries: Vec<&String> = local_images let mut registries: Vec<&String> = local_images.iter().map(|image| &image.registry).collect();
.iter()
.map(|image| &image.registry)
.collect();
registries.unique(); registries.unique();
let mut remote_images: Vec<Image> = Vec::with_capacity(local_images.len()); let mut remote_images: Vec<Image> = Vec::with_capacity(local_images.len());
let client = new_reqwest_client(); let client = new_reqwest_client();
for registry in registries { for registry in registries {
if options.verbose {
debug!("Checking images from registry {}", registry)
}
let images: Vec<&Image> = local_images let images: Vec<&Image> = local_images
.iter() .iter()
.filter(|image| &image.registry == registry) .filter(|image| &image.registry == registry)
.collect(); .collect();
let credentials = config["authentication"][registry] let credentials = options.config["authentication"][registry]
.clone() .clone()
.take_string() .take_string()
.or(None); .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) => { Some(auth_url) => {
let token = get_token(images.clone(), &auth_url, &credentials, &client).await; 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); remote_images.append(&mut latest_images);
} }
if options.verbose {
debug!("Collecting results")
}
let mut result: Vec<(String, Option<bool>)> = Vec::new(); let mut result: Vec<(String, Option<bool>)> = Vec::new();
remote_images.iter().for_each(|image| { remote_images.iter().for_each(|image| {
let img = unsplit_image(&image.registry, &image.repository, &image.tag); let img = unsplit_image(image);
match &image.digest { match &image.digest {
Some(d) => { Some(d) => {
let r = d != image_map.get(&img).unwrap().as_ref().unwrap(); 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)), None => result.push((img, None)),
} }
}); });
let end = Local::now().timestamp_millis();
info!(
"✨ Checked {} images in {}ms",
local_images.len(),
end - start
);
result result
} }
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
pub async fn get_update(image: &str, socket: Option<String>, config: &JsonValue) -> Option<bool> { pub async fn get_update(image: &str, options: &CliConfig) -> Option<bool> {
let local_image = get_image_from_docker_daemon(socket, image).await; let local_image = get_image_from_docker_daemon(options.socket.clone(), image).await;
let credentials = config["authentication"][&local_image.registry] let credentials = options.config["authentication"][&local_image.registry]
.clone() .clone()
.take_string() .take_string()
.or(None); .or(None);
let client = new_reqwest_client(); 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, Some(auth_url) => get_token(vec![&local_image], &auth_url, &credentials, &client).await,
None => String::new(), None => String::new(),
}; };
if options.verbose {
debug!("Using token {}", token);
};
let remote_image = match token.as_str() { let remote_image = match token.as_str() {
"" => get_latest_digest(&local_image, None, config, &client).await, "" => get_latest_digest(&local_image, None, options, &client).await,
_ => get_latest_digest(&local_image, Some(&token), config, &client).await, _ => get_latest_digest(&local_image, Some(&token), options, &client).await,
}; };
match &remote_image.digest { match &remote_image.digest {
Some(d) => Some(d != &local_image.digest.unwrap()), Some(d) => Some(d != &local_image.digest.unwrap()),

View File

@@ -4,7 +4,11 @@ use bollard::{secret::ImageSummary, ClientVersion, Docker};
use bollard::secret::ImageInspect; use bollard::secret::ImageInspect;
use futures::future::join_all; 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<String>) -> Docker { fn create_docker_client(socket: Option<String>) -> Docker {
let client: Result<Docker, bollard::errors::Error> = match socket { let client: Result<Docker, bollard::errors::Error> = match socket {
@@ -25,8 +29,8 @@ fn create_docker_client(socket: Option<String>) -> Docker {
} }
} }
pub async fn get_images_from_docker_daemon(socket: Option<String>) -> Vec<Image> { pub async fn get_images_from_docker_daemon(options: &CliConfig) -> Vec<Image> {
let client: Docker = create_docker_client(socket); let client: Docker = create_docker_client(options.socket.clone());
let images: Vec<ImageSummary> = match client.list_images::<String>(None).await { let images: Vec<ImageSummary> = match client.list_images::<String>(None).await {
Ok(images) => images, Ok(images) => images,
Err(e) => { Err(e) => {
@@ -35,14 +39,14 @@ pub async fn get_images_from_docker_daemon(socket: Option<String>) -> Vec<Image>
}; };
let mut handles = Vec::new(); let mut handles = Vec::new();
for image in images { for image in images {
handles.push(Image::from(image)) handles.push(Image::from(image, options))
}; }
join_all(handles).await.iter().filter(|img| { join_all(handles)
match img { .await
Some(_) => true, .iter()
None => false .filter(|img| img.is_some())
} .map(|img| img.clone().unwrap())
}).map(|img| img.clone().unwrap()).collect() .collect()
} }
#[cfg(feature = "cli")] #[cfg(feature = "cli")]

View File

@@ -8,7 +8,7 @@ use crate::utils::{sort_update_vec, to_json};
pub fn print_updates(updates: &[(String, Option<bool>)], icons: &bool) { pub fn print_updates(updates: &[(String, Option<bool>)], icons: &bool) {
let sorted_updates = sort_update_vec(updates); let sorted_updates = sort_update_vec(updates);
let term_width: usize = termsize::get() 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; .cols as usize;
for update in sorted_updates { for update in sorted_updates {
let description = match update.1 { let description = match update.1 {

View File

@@ -1,6 +1,9 @@
use bollard::secret::ImageSummary; use bollard::secret::ImageSummary;
use crate::utils::split_image; use crate::{
debug,
utils::{split_image, CliConfig},
};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Image { pub struct Image {
@@ -11,25 +14,28 @@ pub struct Image {
} }
impl Image { impl Image {
pub async fn from(image: ImageSummary) -> Option<Self> { pub async fn from(image: ImageSummary, options: &CliConfig) -> Option<Self> {
if !image.repo_tags.is_empty() && !image.repo_digests.is_empty() { if !image.repo_tags.is_empty() && !image.repo_digests.is_empty() {
for t in &image.repo_tags { let (registry, repository, tag) = split_image(&image.repo_tags[0]);
let (registry, repository, tag) = split_image(t); let image = Image {
let image = Image { registry,
registry, repository,
repository, tag,
tag, digest: Some(
digest: Some( image.repo_digests[0]
image.repo_digests[0] .clone()
.clone() .split('@')
.split('@') .collect::<Vec<&str>>()[1]
.collect::<Vec<&str>>()[1] .to_string(),
.to_string(), ),
), };
}; return Some(image);
return Some(image) } else if options.verbose {
} debug!(
"Skipped an image\nTags: {:#?}\nDigests: {:#?}",
image.repo_tags, image.repo_digests
)
} }
None None
} }
} }

View File

@@ -6,7 +6,7 @@ use formatting::{print_raw_update, print_raw_updates, print_update, print_update
#[cfg(feature = "server")] #[cfg(feature = "server")]
use server::serve; use server::serve;
use std::path::PathBuf; use std::path::PathBuf;
use utils::load_config; use utils::{load_config, CliConfig};
pub mod check; pub mod check;
pub mod docker; pub mod docker;
@@ -25,6 +25,13 @@ struct Cli {
socket: Option<String>, socket: Option<String>,
#[arg(short, long, default_value_t = String::new(), help = "Config file path")] #[arg(short, long, default_value_t = String::new(), help = "Config file path")]
config_path: String, config_path: String,
#[arg(
short,
long,
default_value_t = false,
help = "Enable verbose (debug) logging"
)]
verbose: bool,
#[command(subcommand)] #[command(subcommand)]
command: Option<Commands>, command: Option<Commands>,
} }
@@ -64,23 +71,35 @@ async fn main() {
"" => None, "" => None,
path => Some(PathBuf::from(path)), 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 { match &cli.command {
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
Some(Commands::Check { image, icons, raw }) => match image { Some(Commands::Check { image, icons, raw }) => match image {
Some(name) => { Some(name) => {
let has_update = get_update(name, cli.socket, &config).await; let has_update = get_update(name, &cli_config).await;
match raw { match raw {
true => print_raw_update(name, &has_update), true => print_raw_update(name, &has_update),
false => print_update(name, &has_update), false => print_update(name, &has_update),
}; };
} }
None => { None => {
match raw { match *raw || cli.verbose {
true => print_raw_updates(&get_all_updates(cli.socket, &config).await), true => print_raw_updates(&get_all_updates(&cli_config).await),
false => { false => {
let spinner = Spinner::new(); let spinner = Spinner::new();
let updates = get_all_updates(cli.socket, &config).await; let updates = get_all_updates(&cli_config).await;
spinner.succeed(); spinner.succeed();
print_updates(&updates, icons); print_updates(&updates, icons);
} }
@@ -89,7 +108,7 @@ async fn main() {
}, },
#[cfg(feature = "server")] #[cfg(feature = "server")]
Some(Commands::Serve { port }) => { Some(Commands::Serve { port }) => {
let _ = serve(port, cli.socket, config).await; let _ = serve(port, &cli_config).await;
} }
None => (), None => (),
} }

View File

@@ -4,15 +4,28 @@ use json::JsonValue;
use http_auth::parse_challenges; use http_auth::parse_challenges;
use reqwest_middleware::ClientWithMiddleware; 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<String> { pub async fn check_auth(
let protocol = if config["insecure_registries"].contains(registry) { registry: &str,
options: &CliConfig,
client: &ClientWithMiddleware,
) -> Option<String> {
let protocol = if options.config["insecure_registries"].contains(registry) {
if options.verbose {
debug!(
"{} is configured as an insecure registry. Downgrading to HTTP",
registry
);
};
"http" "http"
} else { } else {
"https" "https"
}; };
let response = client.get(&format!("{}://{}/v2/", protocol, registry)).send().await; let response = client
.get(format!("{}://{}/v2/", protocol, registry))
.send()
.await;
match response { match response {
Ok(r) => { Ok(r) => {
let status = r.status().as_u16(); 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 { } else if status == 200 {
None None
} else { } else {
warn!("Received unexpected status code {}\nResponse: {}", status, r.text().await.unwrap()); warn!(
"Received unexpected status code {}\nResponse: {}",
status,
r.text().await.unwrap()
);
None None
} }
}, }
Err(e) => { Err(e) => {
if e.is_connect() { if e.is_connect() {
warn!("Connection to registry {} failed.", &registry); warn!("Connection to registry {} failed.", &registry);
@@ -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 { pub async fn get_latest_digest(
let protocol = image: &Image,
if config["insecure_registries"].contains(json::JsonValue::from(image.registry.clone())) { token: Option<&String>,
"http" options: &CliConfig,
} else { client: &ClientWithMiddleware,
"https" ) -> Image {
}; let protocol = if options.config["insecure_registries"]
let mut request = client.head(&format!( .contains(json::JsonValue::from(image.registry.clone()))
{
"http"
} else {
"https"
};
let mut request = client.head(format!(
"{}://{}/v2/{}/manifests/{}", "{}://{}/v2/{}/manifests/{}",
protocol, &image.registry, &image.repository, &image.tag 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()), digest: Some(digest.to_str().unwrap().to_string()),
..image.clone() ..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( pub async fn get_latest_digests(
images: Vec<&Image>, images: Vec<&Image>,
token: Option<&String>, token: Option<&String>,
config: &JsonValue, options: &CliConfig,
client: &ClientWithMiddleware client: &ClientWithMiddleware,
) -> Vec<Image> { ) -> Vec<Image> {
let mut handles = Vec::new(); let mut handles = Vec::new();
for image in images { 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 join_all(handles).await
} }
pub async fn get_token(images: Vec<&Image>, auth_url: &str, credentials: &Option<String>, client: &ClientWithMiddleware) -> String { pub async fn get_token(
images: Vec<&Image>,
auth_url: &str,
credentials: &Option<String>,
client: &ClientWithMiddleware,
) -> String {
let mut final_url = auth_url.to_owned(); let mut final_url = auth_url.to_owned();
for image in &images { for image in &images {
final_url = format!("{}&scope=repository:{}:pull", final_url, image.repository); final_url = format!("{}&scope=repository:{}:pull", final_url, image.repository);
} }
let mut base_request = let mut base_request = client
client.get(&final_url).header("Accept", "application/vnd.oci.image.index.v1+json"); // Seems to be unnecessary. Will probably remove in the future .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 { base_request = match credentials {
Some(creds) => base_request.header("Authorization", &format!("Basic {}", creds)), Some(creds) => base_request.header("Authorization", &format!("Basic {}", creds)),
None => base_request, None => base_request,

View File

@@ -15,8 +15,8 @@ use xitca_web::{
use crate::{ use crate::{
check::get_all_updates, check::get_all_updates,
error, error, info,
utils::{sort_update_vec, to_json}, utils::{sort_update_vec, to_json, CliConfig},
}; };
const HTML: &str = include_str!("static/index.html"); 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 FAVICON_SVG: &[u8] = include_bytes!("static/favicon.svg");
const APPLE_TOUCH_ICON: &[u8] = include_bytes!("static/apple-touch-icon.png"); const APPLE_TOUCH_ICON: &[u8] = include_bytes!("static/apple-touch-icon.png");
pub async fn serve(port: &u16, socket: Option<String>, config: JsonValue) -> std::io::Result<()> { pub async fn serve(port: &u16, options: &CliConfig) -> std::io::Result<()> {
println!("Starting server, please wait..."); info!("Starting server, please wait...");
let data = ServerData::new(socket, config).await; let data = ServerData::new(options).await;
info!("Ready to start!");
App::new() App::new()
.with_state(Arc::new(Mutex::new(data))) .with_state(Arc::new(Mutex::new(data)))
.at("/", get(handler_service(_static))) .at("/", get(handler_service(_static)))
@@ -93,31 +94,28 @@ struct ServerData {
template: String, template: String,
raw_updates: Vec<(String, Option<bool>)>, raw_updates: Vec<(String, Option<bool>)>,
json: JsonValue, json: JsonValue,
socket: Option<String>, options: CliConfig,
config: JsonValue,
theme: &'static str, theme: &'static str,
} }
impl ServerData { impl ServerData {
async fn new(socket: Option<String>, config: JsonValue) -> Self { async fn new(options: &CliConfig) -> Self {
let mut s = Self { let mut s = Self {
socket, options: options.clone(),
template: String::new(), template: String::new(),
json: json::object! { json: json::object! {
metrics: json::object! {}, metrics: json::object! {},
images: json::object! {}, images: json::object! {},
}, },
raw_updates: Vec::new(), raw_updates: Vec::new(),
config,
theme: "neutral", theme: "neutral",
}; };
s.refresh().await; s.refresh().await;
s s
} }
async fn refresh(&mut self) { async fn refresh(&mut self) {
let updates = sort_update_vec( info!("Refreshing data");
&get_all_updates(self.socket.clone(), &self.config["authentication"]).await, let updates = sort_update_vec(&get_all_updates(&self.options).await);
);
self.raw_updates = updates; self.raw_updates = updates;
let template = liquid::ParserBuilder::with_stdlib() let template = liquid::ParserBuilder::with_stdlib()
.build() .build()
@@ -134,8 +132,11 @@ impl ServerData {
.collect::<Vec<Object>>(); .collect::<Vec<Object>>();
self.json = to_json(&self.raw_updates); self.json = to_json(&self.raw_updates);
let last_updated = Local::now(); let last_updated = Local::now();
self.json["last_updated"] = last_updated.to_rfc3339_opts(chrono::SecondsFormat::Secs, true).to_string().into(); self.json["last_updated"] = last_updated
self.theme = match &self.config["theme"].as_str() { .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
.to_string()
.into();
self.theme = match &self.options.config["theme"].as_str() {
Some(t) => match *t { Some(t) => match *t {
"default" => "neutral", "default" => "neutral",
"blue" => "gray", "blue" => "gray",

View File

@@ -1,27 +1,11 @@
use std::path::PathBuf; use std::path::PathBuf;
use crate::{error, image::Image};
use json::{object, JsonValue}; use json::{object, JsonValue};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff}; use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
/// 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)*));
})
}
static RE: Lazy<Regex> = Lazy::new(|| { static RE: Lazy<Regex> = Lazy::new(|| {
Regex::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 /// 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 { pub fn unsplit_image(image: &Image) -> String {
let reg = match registry { let reg = match image.registry.as_str() {
"registry-1.docker.io" => String::new(), "registry-1.docker.io" => String::new(),
r => format!("{}/", r), r => format!("{}/", r),
}; };
let repo = match repository.split('/').collect::<Vec<&str>>()[0] { let repo = match image.repository.split('/').collect::<Vec<&str>>()[0] {
"library" => { "library" => {
if reg.is_empty() { if reg.is_empty() {
repository.strip_prefix("library/").unwrap() image.repository.strip_prefix("library/").unwrap()
} else { } 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 /// Sorts the update vector alphabetically and where Some(true) > Some(false) > None
@@ -132,10 +116,7 @@ pub fn to_json(updates: &[(String, Option<bool>)]) -> JsonValue {
.iter() .iter()
.filter(|&(_, value)| *value == Some(true)) .filter(|&(_, value)| *value == Some(true))
.count(); .count();
let unknown = updates let unknown = updates.iter().filter(|&(_, value)| value.is_none()).count();
.iter()
.filter(|&(_, value)| value.is_none())
.count();
let _ = json_data["metrics"].insert("monitored_images", updates.len()); 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("up_to_date", up_to_date);
let _ = json_data["metrics"].insert("update_available", update_available); let _ = json_data["metrics"].insert("update_available", update_available);
@@ -143,8 +124,51 @@ pub fn to_json(updates: &[(String, Option<bool>)]) -> JsonValue {
json_data 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<String>,
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 { pub fn new_reqwest_client() -> ClientWithMiddleware {
ClientBuilder::new(reqwest::Client::new()) 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() .build()
} }

View File

@@ -1 +1 @@
nodejs 21.6.2 nodejs 22.8.0