mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-08 05:03:49 -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",
|
||||
"chrono",
|
||||
"clap",
|
||||
"home",
|
||||
"http-auth",
|
||||
"indicatif",
|
||||
"json",
|
||||
@@ -555,6 +556,15 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "http"
|
||||
version = "1.1.0"
|
||||
|
||||
@@ -18,6 +18,7 @@ http-auth = { version = "0.1.9", features = [] }
|
||||
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"
|
||||
|
||||
[features]
|
||||
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};
|
||||
#[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 image::Image;
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
#[cfg(feature = "cli")]
|
||||
use registry::get_latest_digest;
|
||||
use registry::{check_auth, get_latest_digests, get_token};
|
||||
#[cfg(feature = "server")]
|
||||
use server::serve;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Mutex,
|
||||
};
|
||||
use utils::unsplit_image;
|
||||
use std::path::PathBuf;
|
||||
use utils::load_config;
|
||||
|
||||
pub mod check;
|
||||
pub mod docker;
|
||||
#[cfg(feature = "cli")]
|
||||
pub mod formatting;
|
||||
@@ -31,6 +23,8 @@ pub mod utils;
|
||||
struct Cli {
|
||||
#[arg(short, long, default_value = None)]
|
||||
socket: Option<String>,
|
||||
#[arg(short, long, default_value_t = String::new(), help = "Config file path")]
|
||||
config_path: String,
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
@@ -43,34 +37,34 @@ enum Commands {
|
||||
image: Option<String>,
|
||||
#[arg(short, long, default_value_t = false, help = "Enable icons")]
|
||||
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,
|
||||
},
|
||||
#[cfg(feature = "server")]
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
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]
|
||||
async fn main() {
|
||||
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 {
|
||||
#[cfg(feature = "cli")]
|
||||
Some(Commands::Check { image, icons, raw }) => match image {
|
||||
@@ -95,68 +89,8 @@ async fn main() {
|
||||
},
|
||||
#[cfg(feature = "server")]
|
||||
Some(Commands::Serve { port }) => {
|
||||
let _ = serve(port, cli.socket).await;
|
||||
let _ = serve(port, cli.socket, config).await;
|
||||
}
|
||||
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,
|
||||
};
|
||||
|
||||
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 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 APPLE_TOUCH_ICON: &[u8] = include_bytes!("static/apple-touch-icon.png");
|
||||
|
||||
pub async fn serve(port: &u16, socket: Option<String>) -> std::io::Result<()> {
|
||||
let mut data = UpdateData::new(socket).await;
|
||||
pub async fn serve(port: &u16, socket: Option<String>, config: Config) -> std::io::Result<()> {
|
||||
let mut data = ServerData::new(socket, config).await;
|
||||
data.refresh().await;
|
||||
App::new()
|
||||
.with_state(Arc::new(Mutex::new(data)))
|
||||
@@ -38,15 +38,15 @@ pub async fn serve(port: &u16, socket: Option<String>) -> std::io::Result<()> {
|
||||
.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()))
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
|
||||
async fn refresh(data: StateRef<'_, Arc<Mutex<UpdateData>>>) -> WebResponse {
|
||||
async fn refresh(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
||||
data.lock().unwrap().refresh().await;
|
||||
return WebResponse::new(ResponseBody::from("OK"));
|
||||
}
|
||||
@@ -63,32 +63,34 @@ async fn apple_touch_icon() -> WebResponse {
|
||||
WebResponse::new(ResponseBody::from(APPLE_TOUCH_ICON))
|
||||
}
|
||||
|
||||
struct UpdateData {
|
||||
struct ServerData {
|
||||
template: String,
|
||||
raw: Vec<(String, Option<bool>)>,
|
||||
raw_updates: Vec<(String, Option<bool>)>,
|
||||
json: String,
|
||||
socket: Option<String>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl UpdateData {
|
||||
async fn new(socket: Option<String>) -> Self {
|
||||
impl ServerData {
|
||||
async fn new(socket: Option<String>, config: Config) -> Self {
|
||||
return Self {
|
||||
socket,
|
||||
template: String::new(),
|
||||
json: String::new(),
|
||||
raw: Vec::new(),
|
||||
raw_updates: Vec::new(),
|
||||
config,
|
||||
};
|
||||
}
|
||||
async fn refresh(self: &mut Self) {
|
||||
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()
|
||||
.build()
|
||||
.unwrap()
|
||||
.parse(RAW_TEMPLATE)
|
||||
.unwrap();
|
||||
let images = self
|
||||
.raw
|
||||
.raw_updates
|
||||
.iter()
|
||||
.map(|(name, image)| match image {
|
||||
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}],
|
||||
"images": images,
|
||||
"style": STYLE,
|
||||
"last_updated": last_updated.to_string()
|
||||
"last_updated": last_updated.to_string(),
|
||||
"theme": self.config.theme
|
||||
});
|
||||
self.template = template.render(&globals).unwrap();
|
||||
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)),
|
||||
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 {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: #e5e7eb;
|
||||
z-index: 1;
|
||||
{% if theme == "neutral" %}
|
||||
background-color: #e5e5e5;
|
||||
{% elsif theme == "gray" %}
|
||||
background-color: #e5e7eb
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.gi::before,
|
||||
.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;
|
||||
button.disabled = true;
|
||||
|
||||
let request = new XMLHttpRequest()
|
||||
let request = new XMLHttpRequest();
|
||||
request.onload = function () {
|
||||
if (request.status === 200) {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
request.open("GET", `${window.location.origin}/refresh`);
|
||||
request.open('GET', `${window.location.origin}/refresh`);
|
||||
request.send();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<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="max-w-[48rem] mx-auto h-full my-8">
|
||||
<div class="flex items-center gap-1">
|
||||
@@ -130,12 +138,12 @@
|
||||
</g>
|
||||
</svg>
|
||||
</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">
|
||||
{% for metric in metrics %}
|
||||
<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">
|
||||
<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 }}
|
||||
{% if metric.name == 'Monitored images' %}
|
||||
<svg
|
||||
@@ -179,7 +187,7 @@
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
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 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 %}
|
||||
</dl>
|
||||
</div>
|
||||
<div class="shadow-sm bg-white dark:bg-gray-900 rounded-md my-8">
|
||||
<div class="flex justify-between items-center px-6 py-4 text-gray-500">
|
||||
<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-{{ theme }}-500">
|
||||
<h3>Last checked: {{ last_updated }}</h3>
|
||||
<button class="group" onclick="refresh(event)">
|
||||
<svg
|
||||
@@ -214,7 +222,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</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 %}
|
||||
<li>
|
||||
<svg
|
||||
@@ -266,7 +274,7 @@
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="text-gray-500 ml-auto"
|
||||
class="text-{{ theme }}-500 ml-auto"
|
||||
>
|
||||
<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" />
|
||||
|
||||
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 regex::Regex;
|
||||
|
||||
@@ -82,3 +85,56 @@ pub fn sort_update_vec(updates: &[(String, Option<bool>)]) -> Vec<(String, Optio
|
||||
});
|
||||
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: {},
|
||||
},
|
||||
plugins: [],
|
||||
safelist: [
|
||||
{
|
||||
pattern: /(bg|text|divide)-(gray|neutral)-.+/,
|
||||
variants: ["dark"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user