diff --git a/cup.schema.json b/cup.schema.json index 5d1cd57..b4dddec 100644 --- a/cup.schema.json +++ b/cup.schema.json @@ -64,13 +64,13 @@ "minLength": 1 }, "servers": { - "type": "array", + "type": "object", "description": "Additional servers to connect to and fetch update data from", - "minItems": 1, - "items": { + "additionalProperties": { "type": "string", "minLength": 1 - } + }, + "minProperties": 1 }, "theme": { "description": "The theme used by the web UI", diff --git a/src/check.rs b/src/check.rs index 995297c..40b18d1 100644 --- a/src/check.rs +++ b/src/check.rs @@ -10,22 +10,27 @@ use crate::{ registry::{check_auth, get_token}, structs::{image::Image, update::Update}, utils::request::{get_response_body, parse_json}, + warn, }; -/// Fetches image data from other Cup servers -async fn get_remote_updates(servers: &[String], client: &Client) -> Vec { +/// Fetches image data from other Cup instances +async fn get_remote_updates(servers: &FxHashMap, client: &Client) -> Vec { let mut remote_images = Vec::new(); - let futures: Vec<_> = servers + let handles: Vec<_> = servers .iter() - .map(|server| async { - let url = if server.starts_with("http://") || server.starts_with("https://") { - format!("{}/api/v3/json", server.trim_end_matches('/')) + .map(|(name, url)| async { + let url = if url.starts_with("http://") || url.starts_with("https://") { + format!("{}/api/v3/json", url.trim_end_matches('/')) } else { - format!("https://{}/api/v3/json", server.trim_end_matches('/')) + format!("https://{}/api/v3/json", url.trim_end_matches('/')) }; match client.get(&url, vec![], false).await { Ok(response) => { + if response.status() != 200 { + warn!("GET {}: Failed to fetch updates from server. Server returned invalid response code: {}",url,response.status()); + return Vec::new(); + } let json = parse_json(&get_response_body(response).await); if let Some(updates) = json["images"].as_array() { let mut server_updates: Vec = updates @@ -34,7 +39,7 @@ async fn get_remote_updates(servers: &[String], client: &Client) -> Vec .collect(); // Add server origin to each image for update in &mut server_updates { - update.server = Some(server.clone()); + update.server = Some(name.clone()); update.status = update.get_status(); } return server_updates; @@ -42,12 +47,15 @@ async fn get_remote_updates(servers: &[String], client: &Client) -> Vec Vec::new() } - Err(_) => Vec::new(), + Err(e) => { + warn!("Failed to fetch updates from server. {}", e); + Vec::new() + }, } }) .collect(); - for mut images in join_all(futures).await { + for mut images in join_all(handles).await { remote_images.append(&mut images); } @@ -64,8 +72,7 @@ pub async fn get_updates(references: &Option>, config: &Config) -> V // Add extra images from references if let Some(refs) = references { - let image_refs: FxHashSet<&String> = - images.iter().map(|image| &image.reference).collect(); + let image_refs: FxHashSet<&String> = images.iter().map(|image| &image.reference).collect(); let extra = refs .iter() .filter(|&reference| !image_refs.contains(reference)) @@ -85,10 +92,7 @@ pub async fn get_updates(references: &Option>, config: &Config) -> V debug!( config.debug, "Checking {:?}", - images - .iter() - .map(|image| &image.reference) - .collect_vec() + images.iter().map(|image| &image.reference).collect_vec() ); // Get a list of unique registries our images belong to. We are unwrapping the registry because it's guaranteed to be there. @@ -106,7 +110,10 @@ pub async fn get_updates(references: &Option>, config: &Config) -> V let mut image_map: FxHashMap<&String, Vec<&Image>> = FxHashMap::default(); for image in &images { - image_map.entry(&image.parts.registry).or_default().push(image); + image_map + .entry(&image.parts.registry) + .or_default() + .push(image); } // Retrieve an authentication token (if required) for each registry. @@ -149,7 +156,7 @@ pub async fn get_updates(references: &Option>, config: &Config) -> V .collect::>(); let mut handles = Vec::with_capacity(images.len()); - + // Loop through images check for updates for image in &images { let is_ignored = ignored_registries.contains(&&image.parts.registry) diff --git a/src/config.rs b/src/config.rs index 2c43963..cbc3d06 100644 --- a/src/config.rs +++ b/src/config.rs @@ -45,7 +45,7 @@ pub struct Config { pub images: ImageConfig, pub refresh_interval: Option, pub registries: FxHashMap, - pub servers: Vec, + pub servers: FxHashMap, pub socket: Option, pub theme: Theme, } @@ -59,7 +59,7 @@ impl Config { images: ImageConfig::default(), refresh_interval: None, registries: FxHashMap::default(), - servers: Vec::new(), + servers: FxHashMap::default(), socket: None, theme: Theme::Default, } diff --git a/src/registry.rs b/src/registry.rs index a5aa8e5..6301dd5 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -14,7 +14,8 @@ use crate::{ link::parse_link, request::{ get_protocol, get_response_body, parse_json, parse_www_authenticate, to_bearer_string, - }, time::{elapsed, now}, + }, + time::{elapsed, now}, }, }; @@ -173,7 +174,9 @@ pub async fn get_latest_tag( let tag = tags.iter().max(); debug!( config.debug, - "Checked for tag update to {} in {}ms", image.reference, elapsed(start) + "Checked for tag update to {} in {}ms", + image.reference, + elapsed(start) ); match tag { Some(t) => { diff --git a/src/structs/image.rs b/src/structs/image.rs index b1155f6..7cff36e 100644 --- a/src/structs/image.rs +++ b/src/structs/image.rs @@ -188,7 +188,7 @@ impl Image { }, time: self.time_ms, server: None, - status: Status::Unknown(String::new()) + status: Status::Unknown(String::new()), } } diff --git a/src/structs/mod.rs b/src/structs/mod.rs index a0139a7..c673f6e 100644 --- a/src/structs/mod.rs +++ b/src/structs/mod.rs @@ -1,6 +1,6 @@ pub mod image; pub mod inspectdata; +pub mod parts; pub mod status; -pub mod version; pub mod update; -pub mod parts; \ No newline at end of file +pub mod version; diff --git a/src/structs/status.rs b/src/structs/status.rs index cf209d6..f5e81e0 100644 --- a/src/structs/status.rs +++ b/src/structs/status.rs @@ -40,4 +40,4 @@ impl Default for Status { fn default() -> Self { Self::Unknown("".to_string()) } -} \ No newline at end of file +} diff --git a/src/structs/update.rs b/src/structs/update.rs index 819f426..25661fd 100644 --- a/src/structs/update.rs +++ b/src/structs/update.rs @@ -9,7 +9,6 @@ pub struct Update { pub parts: Parts, pub result: UpdateResult, pub time: u32, - #[serde(skip_serializing)] pub server: Option, #[serde(skip_serializing, skip_deserializing)] pub status: Status, @@ -79,19 +78,15 @@ impl Update { Status::Unknown(s) => { if s.is_empty() { match self.result.has_update { - Some(true) => { - match &self.result.info { - UpdateInfo::Version(info) => { - match info.version_update_type.as_str() { - "major" => Status::UpdateMajor, - "minor" => Status::UpdateMinor, - "patch" => Status::UpdatePatch, - _ => unreachable!(), - } - }, - UpdateInfo::Digest(_) => Status::UpdateAvailable, + Some(true) => match &self.result.info { + UpdateInfo::Version(info) => match info.version_update_type.as_str() { + "major" => Status::UpdateMajor, + "minor" => Status::UpdateMinor, + "patch" => Status::UpdatePatch, _ => unreachable!(), - } + }, + UpdateInfo::Digest(_) => Status::UpdateAvailable, + _ => unreachable!(), }, Some(false) => Status::UpToDate, None => Status::Unknown(self.result.error.clone().unwrap()), @@ -99,8 +94,8 @@ impl Update { } else { self.status.clone() } - }, - status => status.clone() + } + status => status.clone(), } } -} \ No newline at end of file +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 1e2f96c..8aa3de6 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,4 +4,4 @@ pub mod logging; pub mod reference; pub mod request; pub mod sort_update_vec; -pub mod time; \ No newline at end of file +pub mod time; diff --git a/src/utils/request.rs b/src/utils/request.rs index ad36521..5bbdd4c 100644 --- a/src/utils/request.rs +++ b/src/utils/request.rs @@ -35,8 +35,8 @@ pub fn get_protocol( } else { "https" } - }, - None => "https" + } + None => "https", } } diff --git a/web/index.html b/web/index.html index d902ba9..8cbf5b9 100644 --- a/web/index.html +++ b/web/index.html @@ -1,34 +1,34 @@ - + {% if theme == 'neutral' %} - - + + {% else %} - - + + {% endif %} - - - - + + + + Cup @@ -39,7 +39,9 @@
-

Cup

+

+ Cup +

- {% assign metrics_to_show = 'monitored_images,up_to_date,updates_available,unknown' | split: ',' %} - {% for metric in metrics %} - {% if metrics_to_show contains metric.name %} -
+
+
-
+
+
-
- {{ metric.name | replace: '_', ' ' | capitalize }} -
-
-
- {{ metric.value }} -
- {% case metric.name %} - {% when 'monitored_images' %} - - - - - {% when 'up_to_date' %} - - - - - {% when 'updates_available' %} - {% assign max_metric = '' %} - {% assign max_value = 0 %} - - {% for m in metrics %} - {% unless metrics_to_show contains m.name %} - {% if m.value > max_value %} - {% assign max_metric = m.name %} - {% assign max_value = m.value %} - {% endif %} - {% endunless %} - {% endfor %} - - {% case max_metric %} - {% when 'major_updates' %} - {% assign color = 'text-red-500' %} - {% when 'minor_updates' %} - {% assign color = 'text-yellow-500' %} - {% else %} - {% assign color = 'text-blue-500' %} - {% endcase %} - - - - - {% when 'unknown' %} - - - - - {% endcase %} -
-
+ {{ metric.value }} + + {% case metric.name %} {% when 'monitored_images' %} + + + + + {% when 'up_to_date' %} + + + + + {% when 'updates_available' %} {% assign max_metric = '' + %} {% assign max_value = 0 %} {% for m in metrics %} {% + unless metrics_to_show contains m.name %} {% if m.value > + max_value %} {% assign max_metric = m.name %} {% assign + max_value = m.value %} {% endif %} {% endunless %} {% + endfor %} {% case max_metric %} {% when 'major_updates' %} + {% assign color = 'text-red-500' %} {% when + 'minor_updates' %} {% assign color = 'text-yellow-500' %} + {% else %} {% assign color = 'text-blue-500' %} {% endcase + %} + + + + + {% when 'unknown' %} + + + + + {% endcase %}
- {% endif %} - {% endfor %} +
+
+ {% endif %} {% endfor %}
{% for image in images %} -
  • - - - - - - - - {{ image.name }} - {% case image.status %} - {% when 'Up to date' %} - - - - - {% when 'Unknown' %} - - - - - {% else %} - {% case image.status %} - {% when 'Major update' %} - {% assign color = 'text-red-500' %} - {% when 'Minor update' %} - {% assign color = 'text-yellow-500' %} - {% else %} - {% assign color = 'text-blue-500' %} - {% endcase %} - - - - - {% endcase %} -
  • +
  • + + + + + + + + {{ image.name }} {% case image.status %} {% when 'Up to date' + %} + + + + + {% when 'Unknown' %} + + + + + {% else %} {% case image.status %} {% when 'Major update' %} + {% assign color = 'text-red-500' %} {% when 'Minor update' %} + {% assign color = 'text-yellow-500' %} {% else %} {% assign + color = 'text-blue-500' %} {% endcase %} + + + + + {% endcase %} +
  • {% endfor %}
    diff --git a/web/src/App.tsx b/web/src/App.tsx index 0c0fd9a..18a49be 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -8,6 +8,7 @@ import { Data } from "./types"; import { theme } from "./theme"; import RefreshButton from "./components/RefreshButton"; import Search from "./components/Search"; +import { Server } from "./components/Server"; const SORT_ORDER = [ "monitored_images", @@ -40,15 +41,17 @@ function App() { className={`bg-white shadow-sm dark:bg-${theme}-900 my-8 rounded-md`} >
    - {Object.entries(data.metrics).sort((a, b) => { - return SORT_ORDER.indexOf(a[0]) - SORT_ORDER.indexOf(b[0]); - }).map(([name]) => ( - - ))} + {Object.entries(data.metrics) + .sort((a, b) => { + return SORT_ORDER.indexOf(a[0]) - SORT_ORDER.indexOf(b[0]); + }) + .map(([name]) => ( + + ))}
    -
      - {data.images - .filter((image) => image.reference.includes(searchQuery)) - .map((image) => ( - +
        + {Object.entries( + data.images.reduce( + (acc, image) => { + const server = image.server ?? ""; + if (!acc[server]) acc[server] = []; + acc[server].push(image); + return acc; + }, + {} as Record, + ), + ) + .sort() + .map(([server, images]) => ( + + {images + .filter((image) => image.reference.includes(searchQuery)) + .map((image) => ( + + ))} + ))}
      diff --git a/web/src/components/Image.tsx b/web/src/components/Image.tsx index 683fd95..40bb729 100644 --- a/web/src/components/Image.tsx +++ b/web/src/components/Image.tsx @@ -51,11 +51,8 @@ export default function Image({ data }: { data: Image }) { } return ( <> -