m/cup
1
0
mirror of https://github.com/sergi0g/cup.git synced 2025-11-08 05:03:49 -05:00

Refactor and simplify server code, UI updates requested in #5 and #6
Some checks failed
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) Has been cancelled
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) Has been cancelled
Nightly Release / build-image (push) Has been cancelled

This commit is contained in:
Sergio
2024-07-13 15:25:39 +03:00
parent 814e0b8133
commit b9278ca010
7 changed files with 202 additions and 72 deletions

1
Cargo.lock generated
View File

@@ -353,6 +353,7 @@ name = "cup"
version = "1.1.3"
dependencies = [
"bollard",
"chrono",
"clap",
"http-auth",
"indicatif",

View File

@@ -17,6 +17,7 @@ once_cell = "1.19.0"
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"] }
[features]
default = ["server", "cli"]

View File

@@ -95,8 +95,7 @@ async fn main() {
},
#[cfg(feature = "server")]
Some(Commands::Serve { port }) => {
let updates = get_all_updates(cli.socket).await;
let _ = serve(port, &updates).await;
let _ = serve(port, cli.socket).await;
}
None => (),
}

View File

@@ -1,27 +1,33 @@
use std::sync::Mutex;
use std::sync::{Arc, Mutex};
use chrono::Local;
use liquid::{object, Object};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use xitca_web::{
body::ResponseBody,
handler::{handler_service, state::StateOwn},
handler::{handler_service, state::StateRef},
http::WebResponse,
middleware::Logger,
route::get,
App,
middleware::Logger
};
use crate::{get_all_updates, utils::sort_update_vec};
const RAW_TEMPLATE: &str = include_str!("static/template.liquid");
const STYLE: &str = include_str!("static/index.css");
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, updates: &[(String, Option<bool>)]) -> std::io::Result<()> {
pub async fn serve(port: &u16, socket: Option<String>) -> std::io::Result<()> {
let mut data = UpdateData::new(socket).await;
data.refresh().await;
App::new()
.with_state(updates.to_owned())
.with_state(Arc::new(Mutex::new(data)))
.at("/", get(handler_service(home)))
.at("/json", get(handler_service(json)))
.at("/refresh", get(handler_service(refresh)))
.at("/favicon.ico", handler_service(favicon_ico)) // These aren't pretty but this is xitca-web...
.at("/favicon.svg", handler_service(favicon_svg))
.at("/apple-touch-icon.png", handler_service(apple_touch_icon))
@@ -32,62 +38,17 @@ pub async fn serve(port: &u16, updates: &[(String, Option<bool>)]) -> std::io::R
.wait()
}
async fn home(
updates: StateOwn<Vec<(String, Option<bool>)>>,
) -> WebResponse {
let template = liquid::ParserBuilder::with_stdlib()
.build()
.unwrap()
.parse(RAW_TEMPLATE)
.unwrap();
let images = updates
.0
.par_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"}),
})
.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();
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}],
"images": images,
"style": STYLE
});
let result = template.render(&globals).unwrap();
WebResponse::new(ResponseBody::from(result))
async fn home(data: StateRef<'_, Arc<Mutex<UpdateData>>>) -> WebResponse {
WebResponse::new(ResponseBody::from(data.lock().unwrap().template.clone()))
}
async fn json(
updates: StateOwn<Vec<(String, Option<bool>)>>
) -> WebResponse {
let result_mutex: Mutex<json::object::Object> = Mutex::new(json::object::Object::new());
updates.par_iter().for_each(|image| match image.1 {
Some(b) => result_mutex.lock().unwrap().insert(&image.0, json::from(b)),
None => result_mutex.lock().unwrap().insert(&image.0, json::Null),
});
let result = json::stringify(result_mutex.lock().unwrap().clone());
WebResponse::new(ResponseBody::from(result))
async fn json(data: StateRef<'_, Arc<Mutex<UpdateData>>>) -> WebResponse {
WebResponse::new(ResponseBody::from(data.lock().unwrap().json.clone()))
}
async fn refresh(data: StateRef<'_, Arc<Mutex<UpdateData>>>) -> WebResponse {
data.lock().unwrap().refresh().await;
return WebResponse::new(ResponseBody::from("OK"));
}
async fn favicon_ico() -> WebResponse {
@@ -100,4 +61,74 @@ async fn favicon_svg() -> WebResponse {
async fn apple_touch_icon() -> WebResponse {
WebResponse::new(ResponseBody::from(APPLE_TOUCH_ICON))
}
}
struct UpdateData {
template: String,
raw: Vec<(String, Option<bool>)>,
json: String,
socket: Option<String>,
}
impl UpdateData {
async fn new(socket: Option<String>) -> Self {
return Self {
socket,
template: String::new(),
json: String::new(),
raw: Vec::new(),
};
}
async fn refresh(self: &mut Self) {
let updates = sort_update_vec(&get_all_updates(self.socket.clone()).await);
self.raw = updates;
let template = liquid::ParserBuilder::with_stdlib()
.build()
.unwrap()
.parse(RAW_TEMPLATE)
.unwrap();
let images = self
.raw
.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"}),
})
.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();
let last_updated = Local::now().format("%Y-%m-%d %H:%M:%S");
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}],
"images": images,
"style": STYLE,
"last_updated": last_updated.to_string()
});
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 {
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());
}
}

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<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" />
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<style>
{{ style }}
</style>
@@ -70,13 +70,28 @@
}
}
</style>
<script>
function refresh(event) {
var button = event.currentTarget;
button.disabled = true;
let request = new XMLHttpRequest()
request.onload = function () {
if (request.status === 200) {
window.location.reload();
}
};
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="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">
<h1 class="text-6xl font-bold dark:text-white">Cup</h1>
<h1 class="text-5xl lg:text-6xl font-bold dark:text-white">Cup</h1>
<svg
version="1.1"
id="Layer_2"
@@ -120,7 +135,57 @@
{% 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">{{ metric.name }}</dt>
<dt class="text-gray-500 dark:text-gray-400 leading-6 font-medium flex gap-1 justify-between">
{{ metric.name }}
{% if metric.name == 'Monitored images' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6 text-black dark:text-white shrink-0"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 4c4.29 0 7.863 2.429 10.665 7.154l.22 .379l.045 .1l.03 .083l.014 .055l.014 .082l.011 .1v.11l-.014 .111a.992 .992 0 0 1 -.026 .11l-.039 .108l-.036 .075l-.016 .03c-2.764 4.836 -6.3 7.38 -10.555 7.499l-.313 .004c-4.396 0 -8.037 -2.549 -10.868 -7.504a1 1 0 0 1 0 -.992c2.831 -4.955 6.472 -7.504 10.868 -7.504zm0 5a3 3 0 1 0 0 6a3 3 0 0 0 0 -6z" />
</svg>
{% elsif metric.name == 'Up to date' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6 text-green-500 shrink-0"
>
<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 metric.name == 'Updates available' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6 text-blue-500 shrink-0"
>
<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 metric.name == 'Unknown' %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
class="size-6 text-gray-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" />
</svg>
{% endif %}
</dt>
<dd class="text-black dark:text-white tracking-tight leading-10 font-medium text-3xl flex-none w-full">
{{ metric.value }}
</dd>
@@ -130,6 +195,25 @@
</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">
<h3>Last checked: {{ last_updated }}</h3>
<button class="group" onclick="refresh(event)">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="group-disabled:animate-spin"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4,11A8.1,8.1 0 0 1 19.5,9M20,5v4h-4" /><path d="M20,13A8.1,8.1 0 0 1 4.5,15M4,19v-4h4" />
</svg>
</button>
</div>
<ul class="*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-gray-800 divide-y dark:text-white">
{% for image in images %}
<li>

View File

@@ -1,6 +1,7 @@
use regex::Regex;
use once_cell::sync::Lazy;
use regex::Regex;
/// 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)*) => ({
@@ -9,7 +10,7 @@ macro_rules! error {
})
}
// Takes an image and splits it into registry, repository and tag. For example ghcr.io/sergi0g/cup:latest becomes ['ghcr.io', 'sergi0g/cup', 'latest'].
/// Takes an image and splits it into registry, repository and tag. For example ghcr.io/sergi0g/cup:latest becomes ['ghcr.io', 'sergi0g/cup', 'latest'].
pub fn split_image(image: &str) -> (String, String, String) {
static RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(
@@ -45,26 +46,39 @@ pub fn split_image(image: &str) -> (String, String, String) {
}
}
/// Given an image's parts which were previously created by split_image, recreate a reference that docker would use. This means removing the registry part, if it's Docker Hub and removing "library" if the image is official
pub fn unsplit_image(registry: &str, repository: &str, tag: &str) -> String {
let reg = match registry {
"registry-1.docker.io" => String::new(),
r => format!("{}/", r),
};
let repo = match repository.split('/').collect::<Vec<&str>>()[0] {
"library" => repository.strip_prefix("library/").unwrap(),
"library" => {
if reg.is_empty() {
repository.strip_prefix("library/").unwrap()
} else {
repository
}
}
_ => repository,
};
format!("{}{}:{}", reg, repo, tag)
}
#[cfg(feature = "cli")]
/// Sorts the update vector alphabetically and where Some(true) > Some(false) > None
pub fn sort_update_vec(updates: &[(String, Option<bool>)]) -> Vec<(String, Option<bool>)> {
let mut sorted_updates = updates.to_vec();
sorted_updates.sort_unstable_by(|a, b| match (a.1, b.1) {
(Some(a), Some(b)) => (!a).cmp(&!b),
(Some(c), Some(d)) => {
if c == d {
a.0.cmp(&b.0)
} else {
(!c).cmp(&!d)
}
}
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
(None, None) => a.0.cmp(&b.0),
});
sorted_updates.to_vec()
}