mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-12 23:23:48 -05:00
Added config, custom theming and moved update checking logic to check.rs
This commit is contained in:
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -355,6 +355,7 @@ dependencies = [
|
|||||||
"bollard",
|
"bollard",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
"home",
|
||||||
"http-auth",
|
"http-auth",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"json",
|
"json",
|
||||||
@@ -555,6 +556,15 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "home"
|
||||||
|
version = "0.5.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ http-auth = { version = "0.1.9", features = [] }
|
|||||||
termsize = { version = "0.1.8", optional = true }
|
termsize = { version = "0.1.8", optional = true }
|
||||||
regex = "1.10.5"
|
regex = "1.10.5"
|
||||||
chrono = { version = "0.4.38", default-features = false, features = ["std", "alloc", "clock"] }
|
chrono = { version = "0.4.38", default-features = false, features = ["std", "alloc", "clock"] }
|
||||||
|
home = "0.5.9"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["server", "cli"]
|
default = ["server", "cli"]
|
||||||
|
|||||||
84
src/check.rs
Normal file
84
src/check.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use std::{collections::{HashMap, HashSet}, sync::Mutex};
|
||||||
|
|
||||||
|
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||||
|
|
||||||
|
use crate::{docker::get_images_from_docker_daemon, image::Image, registry::{check_auth, get_token, get_latest_digests}, utils::unsplit_image};
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
use crate::docker::get_image_from_docker_daemon;
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
use crate::registry::get_latest_digest;
|
||||||
|
|
||||||
|
pub trait Unique<T> {
|
||||||
|
// So we can filter vecs for duplicates
|
||||||
|
fn unique(&mut self);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Unique<T> for Vec<T>
|
||||||
|
where
|
||||||
|
T: Clone + Eq + std::hash::Hash,
|
||||||
|
{
|
||||||
|
fn unique(self: &mut Vec<T>) {
|
||||||
|
let mut seen: HashSet<T> = HashSet::new();
|
||||||
|
self.retain(|item| seen.insert(item.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all_updates(socket: Option<String>) -> Vec<(String, Option<bool>)> {
|
||||||
|
let image_map_mutex: Mutex<HashMap<String, &Option<String>>> = Mutex::new(HashMap::new());
|
||||||
|
let local_images = get_images_from_docker_daemon(socket).await;
|
||||||
|
local_images.par_iter().for_each(|image| {
|
||||||
|
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
|
||||||
|
image_map_mutex.lock().unwrap().insert(img, &image.digest);
|
||||||
|
});
|
||||||
|
let image_map = image_map_mutex.lock().unwrap().clone();
|
||||||
|
let mut registries: Vec<&String> = local_images
|
||||||
|
.par_iter()
|
||||||
|
.map(|image| &image.registry)
|
||||||
|
.collect();
|
||||||
|
registries.unique();
|
||||||
|
let mut remote_images: Vec<Image> = Vec::new();
|
||||||
|
for registry in registries {
|
||||||
|
let images: Vec<&Image> = local_images
|
||||||
|
.par_iter()
|
||||||
|
.filter(|image| &image.registry == registry)
|
||||||
|
.collect();
|
||||||
|
let mut latest_images = match check_auth(registry) {
|
||||||
|
Some(auth_url) => {
|
||||||
|
let token = get_token(images.clone(), &auth_url);
|
||||||
|
get_latest_digests(images, Some(&token))
|
||||||
|
}
|
||||||
|
None => get_latest_digests(images, None),
|
||||||
|
};
|
||||||
|
remote_images.append(&mut latest_images);
|
||||||
|
}
|
||||||
|
let result_mutex: Mutex<Vec<(String, Option<bool>)>> = Mutex::new(Vec::new());
|
||||||
|
remote_images.par_iter().for_each(|image| {
|
||||||
|
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
|
||||||
|
match &image.digest {
|
||||||
|
Some(d) => {
|
||||||
|
let r = d != image_map.get(&img).unwrap().as_ref().unwrap();
|
||||||
|
result_mutex.lock().unwrap().push((img, Some(r)))
|
||||||
|
}
|
||||||
|
None => result_mutex.lock().unwrap().push((img, None)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let result = result_mutex.lock().unwrap().clone();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
pub async fn get_update(image: &str, socket: Option<String>) -> Option<bool> {
|
||||||
|
let local_image = get_image_from_docker_daemon(socket, image).await;
|
||||||
|
let token = match check_auth(&local_image.registry) {
|
||||||
|
Some(auth_url) => get_token(vec![&local_image], &auth_url),
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
let remote_image = match token.as_str() {
|
||||||
|
"" => get_latest_digest(&local_image, None),
|
||||||
|
_ => get_latest_digest(&local_image, Some(&token)),
|
||||||
|
};
|
||||||
|
match &remote_image.digest {
|
||||||
|
Some(d) => Some(d != &local_image.digest.unwrap()),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/main.rs
114
src/main.rs
@@ -1,22 +1,14 @@
|
|||||||
|
use check::{get_all_updates, get_update};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
#[cfg(feature = "cli")]
|
#[cfg(feature = "cli")]
|
||||||
use docker::get_image_from_docker_daemon;
|
|
||||||
use docker::get_images_from_docker_daemon;
|
|
||||||
#[cfg(feature = "cli")]
|
|
||||||
use formatting::{print_raw_update, print_raw_updates, print_update, print_updates, Spinner};
|
use formatting::{print_raw_update, print_raw_updates, print_update, print_updates, Spinner};
|
||||||
use image::Image;
|
|
||||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
|
||||||
#[cfg(feature = "cli")]
|
#[cfg(feature = "cli")]
|
||||||
use registry::get_latest_digest;
|
|
||||||
use registry::{check_auth, get_latest_digests, get_token};
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
use server::serve;
|
use server::serve;
|
||||||
use std::{
|
use std::path::PathBuf;
|
||||||
collections::{HashMap, HashSet},
|
use utils::load_config;
|
||||||
sync::Mutex,
|
|
||||||
};
|
|
||||||
use utils::unsplit_image;
|
|
||||||
|
|
||||||
|
pub mod check;
|
||||||
pub mod docker;
|
pub mod docker;
|
||||||
#[cfg(feature = "cli")]
|
#[cfg(feature = "cli")]
|
||||||
pub mod formatting;
|
pub mod formatting;
|
||||||
@@ -31,6 +23,8 @@ pub mod utils;
|
|||||||
struct Cli {
|
struct Cli {
|
||||||
#[arg(short, long, default_value = None)]
|
#[arg(short, long, default_value = None)]
|
||||||
socket: Option<String>,
|
socket: Option<String>,
|
||||||
|
#[arg(short, long, default_value_t = String::new(), help = "Config file path")]
|
||||||
|
config_path: String,
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<Commands>,
|
command: Option<Commands>,
|
||||||
}
|
}
|
||||||
@@ -43,34 +37,34 @@ enum Commands {
|
|||||||
image: Option<String>,
|
image: Option<String>,
|
||||||
#[arg(short, long, default_value_t = false, help = "Enable icons")]
|
#[arg(short, long, default_value_t = false, help = "Enable icons")]
|
||||||
icons: bool,
|
icons: bool,
|
||||||
#[arg(short, long, default_value_t = false, help = "Output JSON instead of formatted text")]
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
default_value_t = false,
|
||||||
|
help = "Output JSON instead of formatted text"
|
||||||
|
)]
|
||||||
raw: bool,
|
raw: bool,
|
||||||
},
|
},
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
Serve {
|
Serve {
|
||||||
#[arg(short, long, default_value_t = 8000, help = "Use a different port for the server")]
|
#[arg(
|
||||||
|
short,
|
||||||
|
long,
|
||||||
|
default_value_t = 8000,
|
||||||
|
help = "Use a different port for the server"
|
||||||
|
)]
|
||||||
port: u16,
|
port: u16,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Unique<T> {
|
|
||||||
// So we can filter vecs for duplicates
|
|
||||||
fn unique(&mut self);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Unique<T> for Vec<T>
|
|
||||||
where
|
|
||||||
T: Clone + Eq + std::hash::Hash,
|
|
||||||
{
|
|
||||||
fn unique(self: &mut Vec<T>) {
|
|
||||||
let mut seen: HashSet<T> = HashSet::new();
|
|
||||||
self.retain(|item| seen.insert(item.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
let cfg_path = match cli.config_path.as_str() {
|
||||||
|
"" => None,
|
||||||
|
path => Some(PathBuf::from(path)),
|
||||||
|
};
|
||||||
|
let config = load_config(cfg_path);
|
||||||
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 {
|
||||||
@@ -95,68 +89,8 @@ async fn main() {
|
|||||||
},
|
},
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
Some(Commands::Serve { port }) => {
|
Some(Commands::Serve { port }) => {
|
||||||
let _ = serve(port, cli.socket).await;
|
let _ = serve(port, cli.socket, config).await;
|
||||||
}
|
}
|
||||||
None => (),
|
None => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_all_updates(socket: Option<String>) -> Vec<(String, Option<bool>)> {
|
|
||||||
let image_map_mutex: Mutex<HashMap<String, &Option<String>>> = Mutex::new(HashMap::new());
|
|
||||||
let local_images = get_images_from_docker_daemon(socket).await;
|
|
||||||
local_images.par_iter().for_each(|image| {
|
|
||||||
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
|
|
||||||
image_map_mutex.lock().unwrap().insert(img, &image.digest);
|
|
||||||
});
|
|
||||||
let image_map = image_map_mutex.lock().unwrap().clone();
|
|
||||||
let mut registries: Vec<&String> = local_images
|
|
||||||
.par_iter()
|
|
||||||
.map(|image| &image.registry)
|
|
||||||
.collect();
|
|
||||||
registries.unique();
|
|
||||||
let mut remote_images: Vec<Image> = Vec::new();
|
|
||||||
for registry in registries {
|
|
||||||
let images: Vec<&Image> = local_images
|
|
||||||
.par_iter()
|
|
||||||
.filter(|image| &image.registry == registry)
|
|
||||||
.collect();
|
|
||||||
let mut latest_images = match check_auth(registry) {
|
|
||||||
Some(auth_url) => {
|
|
||||||
let token = get_token(images.clone(), &auth_url);
|
|
||||||
get_latest_digests(images, Some(&token))
|
|
||||||
}
|
|
||||||
None => get_latest_digests(images, None),
|
|
||||||
};
|
|
||||||
remote_images.append(&mut latest_images);
|
|
||||||
}
|
|
||||||
let result_mutex: Mutex<Vec<(String, Option<bool>)>> = Mutex::new(Vec::new());
|
|
||||||
remote_images.par_iter().for_each(|image| {
|
|
||||||
let img = unsplit_image(&image.registry, &image.repository, &image.tag);
|
|
||||||
match &image.digest {
|
|
||||||
Some(d) => {
|
|
||||||
let r = d != image_map.get(&img).unwrap().as_ref().unwrap();
|
|
||||||
result_mutex.lock().unwrap().push((img, Some(r)))
|
|
||||||
}
|
|
||||||
None => result_mutex.lock().unwrap().push((img, None)),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let result = result_mutex.lock().unwrap().clone();
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "cli")]
|
|
||||||
async fn get_update(image: &str, socket: Option<String>) -> Option<bool> {
|
|
||||||
let local_image = get_image_from_docker_daemon(socket, image).await;
|
|
||||||
let token = match check_auth(&local_image.registry) {
|
|
||||||
Some(auth_url) => get_token(vec![&local_image], &auth_url),
|
|
||||||
None => String::new(),
|
|
||||||
};
|
|
||||||
let remote_image = match token.as_str() {
|
|
||||||
"" => get_latest_digest(&local_image, None),
|
|
||||||
_ => get_latest_digest(&local_image, Some(&token)),
|
|
||||||
};
|
|
||||||
match &remote_image.digest {
|
|
||||||
Some(d) => Some(d != &local_image.digest.unwrap()),
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ use xitca_web::{
|
|||||||
App,
|
App,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{get_all_updates, utils::sort_update_vec};
|
use crate::{check::get_all_updates, utils::{sort_update_vec, Config}};
|
||||||
|
|
||||||
const RAW_TEMPLATE: &str = include_str!("static/template.liquid");
|
const RAW_TEMPLATE: &str = include_str!("static/template.liquid");
|
||||||
const STYLE: &str = include_str!("static/index.css");
|
const STYLE: &str = include_str!("static/index.css");
|
||||||
@@ -20,8 +20,8 @@ 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>) -> std::io::Result<()> {
|
pub async fn serve(port: &u16, socket: Option<String>, config: Config) -> std::io::Result<()> {
|
||||||
let mut data = UpdateData::new(socket).await;
|
let mut data = ServerData::new(socket, config).await;
|
||||||
data.refresh().await;
|
data.refresh().await;
|
||||||
App::new()
|
App::new()
|
||||||
.with_state(Arc::new(Mutex::new(data)))
|
.with_state(Arc::new(Mutex::new(data)))
|
||||||
@@ -38,15 +38,15 @@ pub async fn serve(port: &u16, socket: Option<String>) -> std::io::Result<()> {
|
|||||||
.wait()
|
.wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn home(data: StateRef<'_, Arc<Mutex<UpdateData>>>) -> WebResponse {
|
async fn home(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
||||||
WebResponse::new(ResponseBody::from(data.lock().unwrap().template.clone()))
|
WebResponse::new(ResponseBody::from(data.lock().unwrap().template.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn json(data: StateRef<'_, Arc<Mutex<UpdateData>>>) -> WebResponse {
|
async fn json(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
||||||
WebResponse::new(ResponseBody::from(data.lock().unwrap().json.clone()))
|
WebResponse::new(ResponseBody::from(data.lock().unwrap().json.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn refresh(data: StateRef<'_, Arc<Mutex<UpdateData>>>) -> WebResponse {
|
async fn refresh(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
||||||
data.lock().unwrap().refresh().await;
|
data.lock().unwrap().refresh().await;
|
||||||
return WebResponse::new(ResponseBody::from("OK"));
|
return WebResponse::new(ResponseBody::from("OK"));
|
||||||
}
|
}
|
||||||
@@ -63,32 +63,34 @@ async fn apple_touch_icon() -> WebResponse {
|
|||||||
WebResponse::new(ResponseBody::from(APPLE_TOUCH_ICON))
|
WebResponse::new(ResponseBody::from(APPLE_TOUCH_ICON))
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UpdateData {
|
struct ServerData {
|
||||||
template: String,
|
template: String,
|
||||||
raw: Vec<(String, Option<bool>)>,
|
raw_updates: Vec<(String, Option<bool>)>,
|
||||||
json: String,
|
json: String,
|
||||||
socket: Option<String>,
|
socket: Option<String>,
|
||||||
|
config: Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateData {
|
impl ServerData {
|
||||||
async fn new(socket: Option<String>) -> Self {
|
async fn new(socket: Option<String>, config: Config) -> Self {
|
||||||
return Self {
|
return Self {
|
||||||
socket,
|
socket,
|
||||||
template: String::new(),
|
template: String::new(),
|
||||||
json: String::new(),
|
json: String::new(),
|
||||||
raw: Vec::new(),
|
raw_updates: Vec::new(),
|
||||||
|
config,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
async fn refresh(self: &mut Self) {
|
async fn refresh(self: &mut Self) {
|
||||||
let updates = sort_update_vec(&get_all_updates(self.socket.clone()).await);
|
let updates = sort_update_vec(&get_all_updates(self.socket.clone()).await);
|
||||||
self.raw = updates;
|
self.raw_updates = updates;
|
||||||
let template = liquid::ParserBuilder::with_stdlib()
|
let template = liquid::ParserBuilder::with_stdlib()
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.parse(RAW_TEMPLATE)
|
.parse(RAW_TEMPLATE)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let images = self
|
let images = self
|
||||||
.raw
|
.raw_updates
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, image)| match image {
|
.map(|(name, image)| match image {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
@@ -121,11 +123,12 @@ impl UpdateData {
|
|||||||
"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": images.len()}, {"name": "Up to date", "value": uptodate}, {"name": "Updates available", "value": updatable}, {"name": "Unknown", "value": unknown}],
|
||||||
"images": images,
|
"images": images,
|
||||||
"style": STYLE,
|
"style": STYLE,
|
||||||
"last_updated": last_updated.to_string()
|
"last_updated": last_updated.to_string(),
|
||||||
|
"theme": self.config.theme
|
||||||
});
|
});
|
||||||
self.template = template.render(&globals).unwrap();
|
self.template = template.render(&globals).unwrap();
|
||||||
let json_data: Mutex<json::object::Object> = Mutex::new(json::object::Object::new());
|
let json_data: Mutex<json::object::Object> = Mutex::new(json::object::Object::new());
|
||||||
self.raw.par_iter().for_each(|image| match image.1 {
|
self.raw_updates.par_iter().for_each(|image| match image.1 {
|
||||||
Some(b) => json_data.lock().unwrap().insert(&image.0, json::from(b)),
|
Some(b) => json_data.lock().unwrap().insert(&image.0, json::from(b)),
|
||||||
None => json_data.lock().unwrap().insert(&image.0, json::Null),
|
None => json_data.lock().unwrap().insert(&image.0, json::Null),
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -21,14 +21,22 @@
|
|||||||
.gi::after {
|
.gi::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #e5e7eb;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
{% if theme == "neutral" %}
|
||||||
|
background-color: #e5e5e5;
|
||||||
|
{% elsif theme == "gray" %}
|
||||||
|
background-color: #e5e7eb
|
||||||
|
{% endif %}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.gi::before,
|
.gi::before,
|
||||||
.gi::after {
|
.gi::after {
|
||||||
background-color: #1f2937;
|
{% if theme == "neutral" %}
|
||||||
|
background-color: #262626;
|
||||||
|
{% elsif theme == "gray" %}
|
||||||
|
background-color: #1f2937
|
||||||
|
{% endif %}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,19 +83,19 @@
|
|||||||
var button = event.currentTarget;
|
var button = event.currentTarget;
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
|
|
||||||
let request = new XMLHttpRequest()
|
let request = new XMLHttpRequest();
|
||||||
request.onload = function () {
|
request.onload = function () {
|
||||||
if (request.status === 200) {
|
if (request.status === 200) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
request.open("GET", `${window.location.origin}/refresh`);
|
request.open('GET', `${window.location.origin}/refresh`);
|
||||||
request.send();
|
request.send();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950">
|
<div class="flex justify-center items-center min-h-screen bg-{{ theme }}-50 dark:bg-{{ theme }}-950">
|
||||||
<div class="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full">
|
<div class="lg:px-8 sm:px-6 px-4 max-w-[80rem] mx-auto h-full w-full">
|
||||||
<div class="max-w-[48rem] mx-auto h-full my-8">
|
<div class="max-w-[48rem] mx-auto h-full my-8">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
@@ -130,12 +138,12 @@
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="shadow-sm bg-white dark:bg-gray-900 rounded-md my-8">
|
<div class="shadow-sm bg-white dark:bg-{{ theme }}-900 rounded-md my-8">
|
||||||
<dl class="lg:grid-cols-4 md:grid-cols-2 gap-1 grid-cols-1 grid overflow-hidden *:relative">
|
<dl class="lg:grid-cols-4 md:grid-cols-2 gap-1 grid-cols-1 grid overflow-hidden *:relative">
|
||||||
{% for metric in metrics %}
|
{% for metric in metrics %}
|
||||||
<div class="gi">
|
<div class="gi">
|
||||||
<div class="xl:px-8 px-6 py-4 gap-y-2 gap-x-4 justify-between align-baseline flex flex-col h-full">
|
<div class="xl:px-8 px-6 py-4 gap-y-2 gap-x-4 justify-between align-baseline flex flex-col h-full">
|
||||||
<dt class="text-gray-500 dark:text-gray-400 leading-6 font-medium flex gap-1 justify-between">
|
<dt class="text-{{ theme }}-500 dark:text-{{ theme }}-400 leading-6 font-medium flex gap-1 justify-between">
|
||||||
{{ metric.name }}
|
{{ metric.name }}
|
||||||
{% if metric.name == 'Monitored images' %}
|
{% if metric.name == 'Monitored images' %}
|
||||||
<svg
|
<svg
|
||||||
@@ -179,7 +187,7 @@
|
|||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="size-6 text-gray-500 shrink-0"
|
class="size-6 text-{{ theme }}-500 shrink-0"
|
||||||
>
|
>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z" />
|
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z" />
|
||||||
@@ -194,8 +202,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="shadow-sm bg-white dark:bg-gray-900 rounded-md my-8">
|
<div class="shadow-sm bg-white dark:bg-{{ theme }}-900 rounded-md my-8">
|
||||||
<div class="flex justify-between items-center px-6 py-4 text-gray-500">
|
<div class="flex justify-between items-center px-6 py-4 text-{{ theme }}-500">
|
||||||
<h3>Last checked: {{ last_updated }}</h3>
|
<h3>Last checked: {{ last_updated }}</h3>
|
||||||
<button class="group" onclick="refresh(event)">
|
<button class="group" onclick="refresh(event)">
|
||||||
<svg
|
<svg
|
||||||
@@ -214,7 +222,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ul class="*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-gray-800 divide-y dark:text-white">
|
<ul class="*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-{{ theme }}-800 divide-y dark:text-white">
|
||||||
{% for image in images %}
|
{% for image in images %}
|
||||||
<li>
|
<li>
|
||||||
<svg
|
<svg
|
||||||
@@ -266,7 +274,7 @@
|
|||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
class="text-gray-500 ml-auto"
|
class="text-{{ theme }}-500 ml-auto"
|
||||||
>
|
>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z" />
|
<path d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z" />
|
||||||
|
|||||||
56
src/utils.rs
56
src/utils.rs
@@ -1,3 +1,6 @@
|
|||||||
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
|
use json::JsonValue;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
@@ -82,3 +85,56 @@ pub fn sort_update_vec(updates: &[(String, Option<bool>)]) -> Vec<(String, Optio
|
|||||||
});
|
});
|
||||||
sorted_updates.to_vec()
|
sorted_updates.to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tries to load the config from the path provided and perform basic validation
|
||||||
|
pub fn load_config(config_path: Option<PathBuf>) -> Config {
|
||||||
|
let raw_config = match &config_path {
|
||||||
|
Some(path) => std::fs::read_to_string(path),
|
||||||
|
None => Ok(String::from("{\"theme\":\"default\"}")),
|
||||||
|
};
|
||||||
|
if raw_config.is_err() {
|
||||||
|
panic!(
|
||||||
|
"Failed to read config file from {}. Are you sure the file exists?",
|
||||||
|
&config_path.unwrap().to_str().unwrap()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let config = match json::parse(&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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Config {
|
||||||
|
pub authentication: HashMap<String, String>,
|
||||||
|
pub theme: String,
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,5 +7,11 @@ module.exports = {
|
|||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
safelist: [
|
||||||
|
{
|
||||||
|
pattern: /(bg|text|divide)-(gray|neutral)-.+/,
|
||||||
|
variants: ["dark"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user