diff --git a/Cargo.lock b/Cargo.lock index 2210370..21d29c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -358,11 +358,12 @@ dependencies = [ "home", "http-auth", "indicatif", - "json", "liquid", "once_cell", "rayon", "regex", + "serde", + "serde_json", "termsize", "tokio", "ureq", @@ -802,12 +803,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" - [[package]] name = "kstring" version = "2.0.0" @@ -1250,18 +1245,18 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 533338b..c4a4b51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ clap = { version = "4.5.7", features = ["derive"] } indicatif = { version = "0.17.8", optional = true } tokio = {version = "1.38.0", features = ["rt", "rt-multi-thread", "macros"]} ureq = { version = "2.9.7", features = ["tls"] } -json = "0.12.4" +serde_json = "1.0" rayon = "1.10.0" xitca-web = { version = "0.5.0", optional = true, features = ["logger"] } liquid = { version = "0.26.6", optional = true } @@ -19,6 +19,7 @@ termsize = { version = "0.1.8", optional = true } regex = "1.10.5" chrono = { version = "0.4.38", default-features = false, features = ["std", "alloc", "clock"] } home = "0.5.9" +serde = "1.0.204" [features] default = ["server", "cli"] @@ -30,4 +31,4 @@ opt-level = "z" strip = "symbols" panic = "abort" lto = "fat" -codegen-units = 1 \ No newline at end of file +codegen-units = 1 diff --git a/src/formatting.rs b/src/formatting.rs index 7132faa..411b777 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -1,9 +1,9 @@ use std::time::Duration; use indicatif::{ProgressBar, ProgressStyle}; -use json::object; +use serde_json::json; -use crate::utils::sort_update_vec; +use crate::utils::{sort_update_vec, to_json}; pub fn print_updates(updates: &[(String, Option)], icons: &bool) { let sorted_updates = sort_update_vec(updates); @@ -40,11 +40,7 @@ pub fn print_updates(updates: &[(String, Option)], icons: &bool) { } pub fn print_raw_updates(updates: &[(String, Option)]) { - let mut result = json::Array::new(); - for update in updates { - result.push(object! {image: update.0.clone(), has_update: update.1}); - } - println!("{}", json::stringify(result)); + println!("{}", serde_json::to_string(&to_json(updates)).unwrap()); } pub fn print_update(name: &str, has_update: &Option) { @@ -62,8 +58,8 @@ pub fn print_update(name: &str, has_update: &Option) { } pub fn print_raw_update(name: &str, has_update: &Option) { - let result = object!{image: name, has_update: *has_update}; - println!("{}", json::stringify(result)); + let result = json!({"images": {name: has_update}}); + println!("{}", result); } pub struct Spinner { diff --git a/src/registry.rs b/src/registry.rs index f33d07b..0d6caa9 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -1,6 +1,7 @@ use std::sync::Mutex; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use serde_json::Value; use ureq::Error; use http_auth::parse_challenges; @@ -34,7 +35,7 @@ pub fn get_latest_digest(image: &Image, token: Option<&String>) -> Image { Ok(response) => response, Err(Error::Status(401, response)) => { if token.is_some() { - error!("Failed to authenticate with given token!\n{}", token.unwrap()) + error!("Failed to authenticate to registry {} with given token!\n{}", &image.registry, token.unwrap()) } else { return get_latest_digest( image, @@ -94,13 +95,18 @@ pub fn get_token(images: Vec<&Image>, auth_url: &str) -> String { error!("Token request failed!\n{}", e) } }; - let parsed_token_response = match json::parse(&raw_response) { + let parsed_token_response: Value = match serde_json::from_str(&raw_response) { Ok(parsed) => parsed, Err(e) => { error!("Failed to parse server response\n{}", e) } }; - parsed_token_response["token"].to_string() + parsed_token_response + .get("token") + .unwrap() + .as_str() + .unwrap() + .to_string() } fn parse_www_authenticate(www_auth: &str) -> String { diff --git a/src/server.rs b/src/server.rs index 8eb1c41..abb442c 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,8 +1,8 @@ -use std::sync::{Arc, Mutex}; +use std::{collections::HashMap, sync::Arc}; use chrono::Local; use liquid::{object, Object}; -use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use tokio::sync::Mutex; use xitca_web::{ body::ResponseBody, handler::{handler_service, state::StateRef}, @@ -12,7 +12,11 @@ use xitca_web::{ App, }; -use crate::{check::get_all_updates, utils::{sort_update_vec, Config}}; +use crate::{ + check::get_all_updates, + error, + utils::{sort_update_vec, to_json, Config, JsonData}, +}; const RAW_TEMPLATE: &str = include_str!("static/template.liquid"); const STYLE: &str = include_str!("static/index.css"); @@ -39,16 +43,18 @@ pub async fn serve(port: &u16, socket: Option, config: Config) -> std::i } async fn home(data: StateRef<'_, Arc>>) -> WebResponse { - WebResponse::new(ResponseBody::from(data.lock().unwrap().template.clone())) + WebResponse::new(ResponseBody::from(data.lock().await.template.clone())) } async fn json(data: StateRef<'_, Arc>>) -> WebResponse { - WebResponse::new(ResponseBody::from(data.lock().unwrap().json.clone())) + WebResponse::new(ResponseBody::from( + serde_json::to_string(&data.lock().await.json).unwrap(), + )) } async fn refresh(data: StateRef<'_, Arc>>) -> WebResponse { - data.lock().unwrap().refresh().await; - return WebResponse::new(ResponseBody::from("OK")); + data.lock().await.refresh().await; + WebResponse::new(ResponseBody::from("OK")) } async fn favicon_ico() -> WebResponse { @@ -66,22 +72,27 @@ async fn apple_touch_icon() -> WebResponse { struct ServerData { template: String, raw_updates: Vec<(String, Option)>, - json: String, + json: JsonData, socket: Option, config: Config, } impl ServerData { async fn new(socket: Option, config: Config) -> Self { - return Self { + let mut s = Self { socket, template: String::new(), - json: String::new(), + json: JsonData { + metrics: HashMap::new(), + images: HashMap::new(), + }, raw_updates: Vec::new(), config, }; + s.refresh().await; + s } - async fn refresh(self: &mut Self) { + async fn refresh(&mut self) { let updates = sort_update_vec(&get_all_updates(self.socket.clone()).await); self.raw_updates = updates; let template = liquid::ParserBuilder::with_stdlib() @@ -92,46 +103,31 @@ impl ServerData { let images = self .raw_updates .iter() - .map(|(name, image)| match image { - Some(value) => { - if *value { - object!({"name": name, "status": "update-available"}) - } else { - object!({"name": name, "status": "up-to-date"}) - } - } - None => object!({"name": name, "status": "unknown"}), + .map(|(name, has_update)| match has_update { + Some(v) => object!({"name": name, "has_update": v.to_string()}), // Liquid kinda thinks false == nil, so we'll be comparing strings from now on + None => object!({"name": name, "has_update": "null"}), }) .collect::>(); - let uptodate = images - .par_iter() - .filter(|&o| o["status"] == "up-to-date") - .collect::>() - .len(); - let updatable = images - .par_iter() - .filter(|&o| o["status"] == "update-available") - .collect::>() - .len(); - let unknown = images - .par_iter() - .filter(|&o| o["status"] == "unknown") - .collect::>() - .len(); + self.json = to_json(&self.raw_updates); let last_updated = Local::now().format("%Y-%m-%d %H:%M:%S"); + let theme = match &self.config.theme { + Some(t) => match t.as_str() { + "default" => "neutral", + "blue" => "gray", + _ => error!( + "Invalid theme {} specified! Please choose between 'default' and 'blue'", + t + ), + }, + None => "neutral", + }; let globals = object!({ - "metrics": [{"name": "Monitored images", "value": images.len()}, {"name": "Up to date", "value": uptodate}, {"name": "Updates available", "value": updatable}, {"name": "Unknown", "value": unknown}], + "metrics": [{"name": "Monitored images", "value": self.json.metrics.get("monitored_images")}, {"name": "Up to date", "value": self.json.metrics.get("up_to_date")}, {"name": "Updates available", "value": self.json.metrics.get("update_available")}, {"name": "Unknown", "value": self.json.metrics.get("unknown")}], "images": images, "style": STYLE, "last_updated": last_updated.to_string(), - "theme": self.config.theme + "theme": theme }); self.template = template.render(&globals).unwrap(); - let json_data: Mutex = Mutex::new(json::object::Object::new()); - self.raw_updates.par_iter().for_each(|image| match image.1 { - Some(b) => json_data.lock().unwrap().insert(&image.0, json::from(b)), - None => json_data.lock().unwrap().insert(&image.0, json::Null), - }); - self.json = json::stringify(json_data.lock().unwrap().clone()); } } diff --git a/src/static/template.liquid b/src/static/template.liquid index e36d2ec..79acbaa 100644 --- a/src/static/template.liquid +++ b/src/static/template.liquid @@ -7,6 +7,7 @@ + Cup @@ -243,7 +244,7 @@ {{ image.name }} - {% if image.status == 'up-to-date' %} + {% if image.has_update == 'false' %} - {% elsif image.status == 'update-available' %} + {% elsif image.has_update == 'true' %} - {% elsif image.status == 'unknown' %} + {% elsif image.has_update == 'null' %} ) -> Config { &config_path.unwrap().to_str().unwrap() ) }; - let config = match json::parse(&raw_config.unwrap()) { + match serde_json::from_str(&raw_config.unwrap()) { Ok(v) => v, Err(e) => panic!("Failed to parse config!\n{}", e), - }; - // Very basic validation - const TOP_LEVEL_KEYS: [&str; 2] = ["authentication", "theme"]; - let themes: JsonValue = json::object! {default: "neutral", blue: "gray"}; - for (key, _) in config.entries() { - if !TOP_LEVEL_KEYS.contains(&key) { - error!("Config contains invalid key {}", key) - } } - if config.has_key("authentication") && !config["authentication"].is_object() { - error!("\"{}\" must be an object", "authentication") - } - for (registry, token) in config["authentication"].entries() { - if !token.is_string() { - error!( - "Invalid token {} for registry {}. Must be a string", - token, registry - ) - } - } - if !themes.has_key(&config["theme"].to_string()) { - error!( - "Invalid theme {}. Available themes are {:#?}", - config["theme"], - themes.entries().map(|(k, _)| k).collect::>() - ) - } - return Config { - authentication: HashMap::new(), - theme: themes[config["theme"].to_string()].to_string(), - }; } +#[derive(Deserialize)] pub struct Config { - pub authentication: HashMap, - pub theme: String, + pub authentication: Option>, + pub theme: Option, +} + +pub fn to_json(updates: &[(String, Option)]) -> JsonData { + let mut json_data: JsonData = JsonData { + metrics: HashMap::new(), + images: HashMap::new(), + }; + updates.iter().for_each(|(image, has_update)| { + let _ = json_data.images.insert(image.clone(), *has_update); + }); + let up_to_date = updates + .iter() + .filter(|&(_, value)| *value == Some(false)) + .collect::)>>() + .len(); + let update_available = updates + .iter() + .filter(|&(_, value)| *value == Some(true)) + .collect::)>>() + .len(); + let unknown = updates + .iter() + .filter(|&(_, value)| value.is_none()) + .collect::)>>() + .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("update_available", update_available); + let _ = json_data.metrics.insert("unknown", unknown); + json_data +} + +#[derive(Serialize)] +pub struct JsonData { + pub metrics: HashMap<&'static str, usize>, + pub images: HashMap>, }