mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-08 05:03:49 -05:00
Added metrics to JSON, switched from json to serde_json, extracted all JSON related functionality into a function in utils.rs
Some checks are pending
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-aarch64 os:ubuntu-latest release_for:linux-aarch64 target:aarch64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-x86_64 os:ubuntu-latest release_for:linux-x86_64 target:x86_64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-image (push) Waiting to run
Some checks are pending
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-aarch64 os:ubuntu-latest release_for:linux-aarch64 target:aarch64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-binary (map[bin:cup command:build name:cup-linux-x86_64 os:ubuntu-latest release_for:linux-x86_64 target:x86_64-unknown-linux-musl]) (push) Waiting to run
Nightly Release / build-image (push) Waiting to run
This commit is contained in:
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
codegen-units = 1
|
||||
|
||||
@@ -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<bool>)], icons: &bool) {
|
||||
let sorted_updates = sort_update_vec(updates);
|
||||
@@ -40,11 +40,7 @@ pub fn print_updates(updates: &[(String, Option<bool>)], icons: &bool) {
|
||||
}
|
||||
|
||||
pub fn print_raw_updates(updates: &[(String, Option<bool>)]) {
|
||||
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<bool>) {
|
||||
@@ -62,8 +58,8 @@ pub fn print_update(name: &str, has_update: &Option<bool>) {
|
||||
}
|
||||
|
||||
pub fn print_raw_update(name: &str, has_update: &Option<bool>) {
|
||||
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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<String>, config: Config) -> std::i
|
||||
}
|
||||
|
||||
async fn home(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> 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<Mutex<ServerData>>>) -> 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<Mutex<ServerData>>>) -> 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<bool>)>,
|
||||
json: String,
|
||||
json: JsonData,
|
||||
socket: Option<String>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl ServerData {
|
||||
async fn new(socket: Option<String>, 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::<Vec<Object>>();
|
||||
let uptodate = images
|
||||
.par_iter()
|
||||
.filter(|&o| o["status"] == "up-to-date")
|
||||
.collect::<Vec<&Object>>()
|
||||
.len();
|
||||
let updatable = images
|
||||
.par_iter()
|
||||
.filter(|&o| o["status"] == "update-available")
|
||||
.collect::<Vec<&Object>>()
|
||||
.len();
|
||||
let unknown = images
|
||||
.par_iter()
|
||||
.filter(|&o| o["status"] == "unknown")
|
||||
.collect::<Vec<&Object>>()
|
||||
.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<json::object::Object> = 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Cup</title>
|
||||
<style>
|
||||
{{ style }}
|
||||
</style>
|
||||
@@ -243,7 +244,7 @@
|
||||
<path d="M3.27 6.96l8.73 5.04" />
|
||||
</svg>
|
||||
{{ image.name }}
|
||||
{% if image.status == 'up-to-date' %}
|
||||
{% if image.has_update == 'false' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
@@ -255,7 +256,7 @@
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z" />
|
||||
</svg>
|
||||
{% elsif image.status == 'update-available' %}
|
||||
{% elsif image.has_update == 'true' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
@@ -267,7 +268,7 @@
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z" />
|
||||
</svg>
|
||||
{% elsif image.status == 'unknown' %}
|
||||
{% elsif image.has_update == 'null' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
|
||||
77
src/utils.rs
77
src/utils.rs
@@ -1,8 +1,8 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use json::JsonValue;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 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]
|
||||
@@ -98,43 +98,52 @@ pub fn load_config(config_path: Option<PathBuf>) -> 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::<Vec<&str>>()
|
||||
)
|
||||
}
|
||||
return Config {
|
||||
authentication: HashMap::new(),
|
||||
theme: themes[config["theme"].to_string()].to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub authentication: HashMap<String, String>,
|
||||
pub theme: String,
|
||||
pub authentication: Option<HashMap<String, String>>,
|
||||
pub theme: Option<String>,
|
||||
}
|
||||
|
||||
pub fn to_json(updates: &[(String, Option<bool>)]) -> 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::<Vec<&(String, Option<bool>)>>()
|
||||
.len();
|
||||
let update_available = updates
|
||||
.iter()
|
||||
.filter(|&(_, value)| *value == Some(true))
|
||||
.collect::<Vec<&(String, Option<bool>)>>()
|
||||
.len();
|
||||
let unknown = updates
|
||||
.iter()
|
||||
.filter(|&(_, value)| value.is_none())
|
||||
.collect::<Vec<&(String, Option<bool>)>>()
|
||||
.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<String, Option<bool>>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user