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 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<String>,
config: &JsonValue,
) -> Vec<(String, Option<bool>)> {
let local_images = get_images_from_docker_daemon(socket).await;
pub async fn get_all_updates(options: &CliConfig) -> Vec<(String, Option<bool>)> {
let start = Local::now().timestamp_millis();
let local_images = get_images_from_docker_daemon(options).await;
let mut image_map: HashMap<String, Option<String>> = 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<Image> = 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);
}
None => get_latest_digests(images, None, config, &client).await,
get_latest_digests(images, Some(&token), options, &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<bool>)> = 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<String>, config: &JsonValue) -> Option<bool> {
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<bool> {
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()),

View File

@@ -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<String>) -> Docker {
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> {
let client: Docker = create_docker_client(socket);
pub async fn get_images_from_docker_daemon(options: &CliConfig) -> Vec<Image> {
let client: Docker = create_docker_client(options.socket.clone());
let images: Vec<ImageSummary> = match client.list_images::<String>(None).await {
Ok(images) => images,
Err(e) => {
@@ -35,14 +39,14 @@ pub async fn get_images_from_docker_daemon(socket: Option<String>) -> Vec<Image>
};
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
handles.push(Image::from(image, options))
}
}).map(|img| img.clone().unwrap()).collect()
join_all(handles)
.await
.iter()
.filter(|img| img.is_some())
.map(|img| img.clone().unwrap())
.collect()
}
#[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) {
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 {

View File

@@ -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,10 +14,9 @@ pub struct 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() {
for t in &image.repo_tags {
let (registry, repository, tag) = split_image(t);
let (registry, repository, tag) = split_image(&image.repo_tags[0]);
let image = Image {
registry,
repository,
@@ -27,8 +29,12 @@ impl Image {
.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
}

View File

@@ -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<String>,
#[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<Commands>,
}
@@ -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 => (),
}

View File

@@ -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<String> {
let protocol = if config["insecure_registries"].contains(registry) {
pub async fn check_auth(
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"
} 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.", &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 {
let protocol =
if config["insecure_registries"].contains(json::JsonValue::from(image.registry.clone())) {
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!(
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<Image> {
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<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();
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,

View File

@@ -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<String>, 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<bool>)>,
json: JsonValue,
socket: Option<String>,
config: JsonValue,
options: CliConfig,
theme: &'static str,
}
impl ServerData {
async fn new(socket: Option<String>, 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::<Vec<Object>>();
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",

View File

@@ -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<Regex> = 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::<Vec<&str>>()[0] {
let repo = match image.repository.split('/').collect::<Vec<&str>>()[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<bool>)]) -> 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<bool>)]) -> 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<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 {
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()
}

View File

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