m/cup
1
0
mirror of https://github.com/sergi0g/cup.git synced 2025-11-17 01:23:39 -05:00

Refactor logging, create context for passing around between functions instead of config

This commit is contained in:
Sergio
2025-02-14 19:24:35 +02:00
parent 6ae95bf83b
commit 550fb955a3
11 changed files with 156 additions and 137 deletions

View File

@@ -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<String, String>, client: &Client) -> Vec<Update> {
async fn get_remote_updates(ctx: &Context, client: &Client) -> Vec<Update> {
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<String, String>, 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<String, String>, 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<String, String>, client: &Client
}
/// Returns a list of updates for all images passed in.
pub async fn get_updates(references: &Option<Vec<String>>, config: &Config) -> Vec<Update> {
let client = Client::new();
pub async fn get_updates(references: &Option<Vec<String>>, ctx: &Context) -> Vec<Update> {
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<Vec<String>>, 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<Vec<String>>, 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<Vec<String>>, config: &Config) -> V
// Retrieve an authentication token (if required) for each registry.
let mut tokens: FxHashMap<&str, Option<String>> = 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) {
&registry_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<Vec<String>>, 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<Vec<String>>, 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);
}
}

View File

@@ -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<String>,
pub registries: FxHashMap<String, RegistryConfig>,
@@ -55,7 +53,6 @@ impl Config {
Self {
version: 3,
agent: false,
debug: false,
images: ImageConfig::default(),
refresh_interval: None,
registries: FxHashMap::default(),

View File

@@ -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<Docker, bollard::errors::Error> = 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<String>>,
) -> Vec<Image> {
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());

View File

@@ -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()
}
}

42
src/logging.rs Normal file
View File

@@ -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<str>) {
if !self.raw {
eprintln!("\x1b[33;1m WARN\x1b[0m {}", msg.as_ref());
}
}
pub fn info(&self, msg: impl AsRef<str>) {
if !self.raw {
println!("\x1b[36;1m INFO\x1b[0m {}", msg.as_ref());
}
}
pub fn debug(&self, msg: impl AsRef<str>) {
if self.debug {
println!("\x1b[35;1mDEBUG\x1b[0m {}", msg.as_ref());
}
}
pub fn set_raw(&mut self, raw: bool) {
self.raw = raw
}
}

View File

@@ -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."),
}

View File

@@ -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<String> {
let protocol = get_protocol(registry, &config.registries);
pub async fn check_auth(registry: &str, ctx: &Context, client: &Client) -> Option<String> {
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

View File

@@ -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<Update>,
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",
};

View File

@@ -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!(),
},
}

View File

@@ -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)*));
}
})
}

View File

@@ -1,6 +1,5 @@
pub mod json;
pub mod link;
pub mod logging;
pub mod reference;
pub mod request;
pub mod sort_update_vec;