From aeeffaccbaaf26cff15d59cc9179f79739651c07 Mon Sep 17 00:00:00 2001 From: Sergio <77530549+sergi0g@users.noreply.github.com> Date: Thu, 2 Jan 2025 20:11:31 +0200 Subject: [PATCH] Get updates from multiple servers (part one: get the data) --- src/check.rs | 120 +++++++++++++++++++++++++---------- src/formatting/mod.rs | 12 ++-- src/registry.rs | 10 +-- src/server.rs | 10 +-- src/structs/image.rs | 108 +++++++++++++++++-------------- src/structs/mod.rs | 2 + src/structs/parts.rs | 8 +++ src/structs/status.rs | 9 ++- src/structs/update.rs | 106 +++++++++++++++++++++++++++++++ src/utils/json.rs | 29 +++++---- src/utils/sort_update_vec.rs | 104 +++++++++--------------------- web/src/App.tsx | 15 ++++- web/src/components/Image.tsx | 2 +- 13 files changed, 348 insertions(+), 187 deletions(-) create mode 100644 src/structs/parts.rs create mode 100644 src/structs/update.rs diff --git a/src/check.rs b/src/check.rs index b968c55..995297c 100644 --- a/src/check.rs +++ b/src/check.rs @@ -8,44 +8,93 @@ use crate::{ docker::get_images_from_docker_daemon, http::Client, registry::{check_auth, get_token}, - structs::image::Image, + structs::{image::Image, update::Update}, + utils::request::{get_response_body, parse_json}, }; +/// Fetches image data from other Cup servers +async fn get_remote_updates(servers: &[String], client: &Client) -> Vec { + let mut remote_images = Vec::new(); + + let futures: 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('/')) + } else { + format!("https://{}/api/v3/json", server.trim_end_matches('/')) + }; + match client.get(&url, vec![], false).await { + Ok(response) => { + let json = parse_json(&get_response_body(response).await); + if let Some(updates) = json["images"].as_array() { + let mut server_updates: Vec = updates + .iter() + .filter_map(|img| serde_json::from_value(img.clone()).ok()) + .collect(); + // Add server origin to each image + for update in &mut server_updates { + update.server = Some(server.clone()); + update.status = update.get_status(); + } + return server_updates; + } + + Vec::new() + } + Err(_) => Vec::new(), + } + }) + .collect(); + + for mut images in join_all(futures).await { + remote_images.append(&mut images); + } + + remote_images +} + /// Returns a list of updates for all images passed in. -pub async fn get_updates(references: &Option>, config: &Config) -> Vec { - // Get images +pub async fn get_updates(references: &Option>, config: &Config) -> Vec { + let client = Client::new(); + + // Get local images debug!(config.debug, "Retrieving images to be checked"); let mut images = get_images_from_docker_daemon(config, references).await; - let extra_images = match references { - Some(refs) => { - let image_refs: FxHashSet<&String> = - images.iter().map(|image| &image.reference).collect(); - let extra = refs - .iter() - .filter(|&reference| !image_refs.contains(reference)) - .collect::>(); - Some( - extra - .iter() - .map(|reference| Image::from_reference(reference)) - .collect::>(), - ) - } - None => None, - }; - if let Some(extra_imgs) = extra_images { - images.extend_from_slice(&extra_imgs); + + // Add extra images from references + if let Some(refs) = references { + let image_refs: FxHashSet<&String> = + images.iter().map(|image| &image.reference).collect(); + let extra = refs + .iter() + .filter(|&reference| !image_refs.contains(reference)) + .map(|reference| Image::from_reference(reference)) + .collect::>(); + images.extend(extra); } + + // Get remote images from other servers + let remote_updates = if !config.servers.is_empty() { + debug!(config.debug, "Fetching updates from remote servers"); + get_remote_updates(&config.servers, &client).await + } else { + Vec::new() + }; + 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. let registries: Vec<&String> = images .iter() - .map(|image| &image.registry) + .map(|image| &image.parts.registry) .unique() .collect::>(); @@ -57,7 +106,7 @@ 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.registry).or_default().push(image); + image_map.entry(&image.parts.registry).or_default().push(image); } // Retrieve an authentication token (if required) for each registry. @@ -87,9 +136,6 @@ pub async fn get_updates(references: &Option>, config: &Config) -> V debug!(config.debug, "Tokens: {:?}", tokens); - // Create a Vec to store futures so we can await them all at once. - let mut handles = Vec::with_capacity(images.len()); - let ignored_registries = config .registries .iter() @@ -102,15 +148,25 @@ pub async fn get_updates(references: &Option>, config: &Config) -> V }) .collect::>(); - // Loop through images and get the latest digest for each + 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.registry) || config.images.exclude.iter().any(|item| image.reference.starts_with(item)); + let is_ignored = ignored_registries.contains(&&image.parts.registry) + || config + .images + .exclude + .iter() + .any(|item| image.reference.starts_with(item)); if !is_ignored { - let token = tokens.get(image.registry.as_str()).unwrap(); + let token = tokens.get(image.parts.registry.as_str()).unwrap(); let future = image.check(token.as_deref(), config, &client); handles.push(future); } } // Await all the futures - join_all(handles).await + let images = join_all(handles).await; + let mut updates: Vec = images.iter().map(|image| image.to_update()).collect(); + updates.extend_from_slice(&remote_updates); + updates } diff --git a/src/formatting/mod.rs b/src/formatting/mod.rs index d738f16..2d2479b 100644 --- a/src/formatting/mod.rs +++ b/src/formatting/mod.rs @@ -1,17 +1,17 @@ pub mod spinner; use crate::{ - structs::{image::Image, status::Status}, - utils::{json::to_simple_json, sort_update_vec::sort_image_vec}, + structs::{status::Status, update::Update}, + utils::{json::to_simple_json, sort_update_vec::sort_update_vec}, }; -pub fn print_updates(updates: &[Image], icons: &bool) { - let sorted_images = sort_image_vec(updates); +pub fn print_updates(updates: &[Update], icons: &bool) { + let sorted_images = sort_update_vec(updates); let term_width: usize = termsize::get() .unwrap_or(termsize::Size { rows: 24, cols: 80 }) .cols as usize; for image in sorted_images { - let has_update = image.has_update(); + let has_update = image.get_status(); let description = has_update.to_string(); let icon = if *icons { match has_update { @@ -38,6 +38,6 @@ pub fn print_updates(updates: &[Image], icons: &bool) { } } -pub fn print_raw_updates(updates: &[Image]) { +pub fn print_raw_updates(updates: &[Update]) { println!("{}", to_simple_json(updates)); } diff --git a/src/registry.rs b/src/registry.rs index c3ae73b..a5aa8e5 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -52,10 +52,10 @@ pub async fn get_latest_digest( "Checking for digest update to {}", image.reference ); let start = SystemTime::now(); - let protocol = get_protocol(&image.registry, &config.registries); + let protocol = get_protocol(&image.parts.registry, &config.registries); let url = format!( "{}://{}/v2/{}/manifests/{}", - protocol, &image.registry, &image.repository, &image.tag + protocol, &image.parts.registry, &image.parts.repository, &image.parts.tag ); let authorization = to_bearer_string(&token); let headers = vec![("Accept", Some("application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.index.v1+json")), ("Authorization", authorization.as_deref())]; @@ -103,7 +103,7 @@ pub async fn get_token( ) -> String { let mut url = auth_url.to_owned(); for image in images { - url = format!("{}&scope=repository:{}:pull", url, image.repository); + url = format!("{}&scope=repository:{}:pull", url, image.parts.repository); } let authorization = credentials.as_ref().map(|creds| format!("Basic {}", creds)); let headers = vec![("Authorization", authorization.as_deref())]; @@ -128,10 +128,10 @@ pub async fn get_latest_tag( "Checking for tag update to {}", image.reference ); let start = now(); - let protocol = get_protocol(&image.registry, &config.registries); + let protocol = get_protocol(&image.parts.registry, &config.registries); let url = format!( "{}://{}/v2/{}/tags/list", - protocol, &image.registry, &image.repository, + protocol, &image.parts.registry, &image.parts.repository, ); let authorization = to_bearer_string(&token); let headers = vec![ diff --git a/src/server.rs b/src/server.rs index f74d0b7..ba13810 100644 --- a/src/server.rs +++ b/src/server.rs @@ -19,10 +19,10 @@ use crate::{ check::get_updates, config::{Config, Theme}, info, - structs::image::Image, + structs::update::Update, utils::{ json::{to_full_json, to_simple_json}, - sort_update_vec::sort_image_vec, + sort_update_vec::sort_update_vec, time::{elapsed, now}, }, }; @@ -147,7 +147,7 @@ async fn refresh(data: StateRef<'_, Arc>>) -> WebResponse { struct ServerData { template: String, - raw_updates: Vec, + raw_updates: Vec, simple_json: Value, full_json: Value, config: Config, @@ -172,7 +172,7 @@ impl ServerData { if !self.raw_updates.is_empty() { info!("Refreshing data"); } - let updates = sort_image_vec(&get_updates(&None, &self.config).await); + let updates = sort_update_vec(&get_updates(&None, &self.config).await); info!( "✨ Checked {} images in {}ms", updates.len(), @@ -187,7 +187,7 @@ impl ServerData { let images = self .raw_updates .iter() - .map(|image| object!({"name": image.reference, "status": image.has_update().to_string()}),) + .map(|image| object!({"name": image.reference, "status": image.get_status().to_string()}),) .collect::>(); self.simple_json = to_simple_json(&self.raw_updates); self.full_json = to_full_json(&self.raw_updates); diff --git a/src/structs/image.rs b/src/structs/image.rs index 1ceb5ef..b1155f6 100644 --- a/src/structs/image.rs +++ b/src/structs/image.rs @@ -1,5 +1,3 @@ -use serde_json::{json, Value}; - use crate::{ config::Config, error, @@ -9,7 +7,11 @@ use crate::{ utils::reference::split, }; -use super::inspectdata::InspectData; +use super::{ + inspectdata::InspectData, + parts::Parts, + update::{DigestUpdateInfo, Update, UpdateInfo, UpdateResult, VersionUpdateInfo}, +}; #[derive(Clone, PartialEq)] #[cfg_attr(test, derive(Debug))] @@ -23,7 +25,7 @@ pub struct DigestInfo { pub struct VersionInfo { pub current_tag: Version, pub latest_remote_tag: Option, - pub format_str: String + pub format_str: String, } /// Image struct that contains all information that may be needed by a function working with an image. @@ -32,9 +34,7 @@ pub struct VersionInfo { #[cfg_attr(test, derive(Debug))] pub struct Image { pub reference: String, - pub registry: String, - pub repository: String, - pub tag: String, + pub parts: Parts, pub digest_info: Option, pub version_info: Option, pub error: Option, @@ -56,9 +56,11 @@ impl Image { .collect(); Some(Self { reference, - registry, - repository, - tag, + parts: Parts { + registry, + repository, + tag, + }, digest_info: Some(DigestInfo { local_digests, remote_digest: None, @@ -82,9 +84,11 @@ impl Image { match version_tag { Some((version, format_str)) => Self { reference: reference.to_string(), - registry, - repository, - tag, + parts: Parts { + registry, + repository, + tag, + }, version_info: Some(VersionInfo { current_tag: version, format_str, @@ -127,59 +131,65 @@ impl Image { } } - /// Converts image data into a `Value` - pub fn to_json(&self) -> Value { + /// Converts image data into an `Update` + pub fn to_update(&self) -> Update { let has_update = self.has_update(); let update_type = match has_update { Status::UpdateMajor | Status::UpdateMinor | Status::UpdatePatch => "version", _ => "digest", }; - json!({ - "reference": self.reference.clone(), - "parts": { - "registry": self.registry.clone(), - "repository": self.repository.clone(), - "tag": self.tag.clone() - }, - "result": { - "has_update": has_update.to_option_bool(), - "info": match has_update { - Status::Unknown(_) => None, - _ => Some(match update_type { + Update { + reference: self.reference.clone(), + parts: self.parts.clone(), + result: UpdateResult { + has_update: has_update.to_option_bool(), + info: match has_update { + Status::Unknown(_) => UpdateInfo::None, + _ => match update_type { "version" => { - let (version_tag, latest_remote_tag) = match &self.version_info { - Some(data) => (data.current_tag.clone(), data.latest_remote_tag.clone()), - _ => unreachable!() + let (new_tag, format_str) = match &self.version_info { + Some(data) => ( + data.latest_remote_tag.clone().unwrap(), + data.format_str.clone(), + ), + _ => unreachable!(), }; - json!({ - "type": update_type, - "version_update_type": match has_update { + + UpdateInfo::Version(VersionUpdateInfo { + version_update_type: match has_update { Status::UpdateMajor => "major", Status::UpdateMinor => "minor", Status::UpdatePatch => "patch", - _ => unreachable!() - }, - "new_version": self.tag.replace(&version_tag.to_string(), &latest_remote_tag.as_ref().unwrap().to_string()) + _ => unreachable!(), + } + .to_string(), + new_version: format_str + .replacen("{}", &new_tag.major.to_string(), 1) + .replacen("{}", &new_tag.minor.unwrap_or(0).to_string(), 1) + .replacen("{}", &new_tag.patch.unwrap_or(0).to_string(), 1), }) - }, + } "digest" => { let (local_digests, remote_digest) = match &self.digest_info { - Some(data) => (data.local_digests.clone(), data.remote_digest.clone()), - _ => unreachable!() + Some(data) => { + (data.local_digests.clone(), data.remote_digest.clone()) + } + _ => unreachable!(), }; - json!({ - "type": update_type, - "local_digests": local_digests, - "remote_digest": remote_digest, + UpdateInfo::Digest(DigestUpdateInfo { + local_digests, + remote_digest, }) - }, - _ => unreachable!() - }), + } + _ => unreachable!(), + }, }, - "error": self.error.clone() + error: self.error.clone(), }, - "time": self.time_ms - }) + time: self.time_ms, + server: None, + status: Status::Unknown(String::new()) + } } /// Checks if the image has an update diff --git a/src/structs/mod.rs b/src/structs/mod.rs index 42235fa..a0139a7 100644 --- a/src/structs/mod.rs +++ b/src/structs/mod.rs @@ -2,3 +2,5 @@ pub mod image; pub mod inspectdata; pub mod status; pub mod version; +pub mod update; +pub mod parts; \ No newline at end of file diff --git a/src/structs/parts.rs b/src/structs/parts.rs new file mode 100644 index 0000000..8a4e336 --- /dev/null +++ b/src/structs/parts.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +pub struct Parts { + pub registry: String, + pub repository: String, + pub tag: String, +} diff --git a/src/structs/status.rs b/src/structs/status.rs index 6801378..cf209d6 100644 --- a/src/structs/status.rs +++ b/src/structs/status.rs @@ -1,7 +1,8 @@ use std::fmt::Display; /// Enum for image status -#[derive(Ord, Eq, PartialEq, PartialOrd)] +#[derive(Ord, Eq, PartialEq, PartialOrd, Clone)] +#[cfg_attr(test, derive(Debug))] pub enum Status { UpdateMajor, UpdateMinor, @@ -34,3 +35,9 @@ impl Status { } } } + +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 new file mode 100644 index 0000000..819f426 --- /dev/null +++ b/src/structs/update.rs @@ -0,0 +1,106 @@ +use serde::{ser::SerializeStruct, Deserialize, Serialize}; + +use super::{parts::Parts, status::Status}; + +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(test, derive(PartialEq, Debug, Default))] +pub struct Update { + pub reference: String, + pub parts: Parts, + pub result: UpdateResult, + pub time: u32, + #[serde(skip_serializing)] + pub server: Option, + #[serde(skip_serializing, skip_deserializing)] + pub status: Status, +} + +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(test, derive(PartialEq, Debug, Default))] +pub struct UpdateResult { + pub has_update: Option, + pub info: UpdateInfo, + pub error: Option, +} + +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(test, derive(PartialEq, Debug, Default))] +#[serde(untagged)] +pub enum UpdateInfo { + #[cfg_attr(test, default)] + None, + Version(VersionUpdateInfo), + Digest(DigestUpdateInfo), +} + +#[derive(Deserialize, Clone)] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub struct VersionUpdateInfo { + pub version_update_type: String, + pub new_version: String, +} + +#[derive(Deserialize, Clone)] +#[cfg_attr(test, derive(PartialEq, Debug))] +pub struct DigestUpdateInfo { + pub local_digests: Vec, + pub remote_digest: Option, +} + +impl Serialize for VersionUpdateInfo { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut state = serializer.serialize_struct("VersionUpdateInfo", 3)?; + let _ = state.serialize_field("type", "version"); + let _ = state.serialize_field("version_update_type", &self.version_update_type); + let _ = state.serialize_field("new_version", &self.new_version); + state.end() + } +} + +impl Serialize for DigestUpdateInfo { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut state = serializer.serialize_struct("DigestUpdateInfo", 3)?; + let _ = state.serialize_field("type", "digest"); + let _ = state.serialize_field("local_digests", &self.local_digests); + let _ = state.serialize_field("remote_digest", &self.remote_digest); + state.end() + } +} + +impl Update { + pub fn get_status(&self) -> Status { + match &self.status { + 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, + _ => unreachable!(), + } + }, + Some(false) => Status::UpToDate, + None => Status::Unknown(self.result.error.clone().unwrap()), + } + } else { + self.status.clone() + } + }, + status => status.clone() + } + } +} \ No newline at end of file diff --git a/src/utils/json.rs b/src/utils/json.rs index ca27c01..3a38cfe 100644 --- a/src/utils/json.rs +++ b/src/utils/json.rs @@ -2,10 +2,10 @@ use serde_json::{json, Map, Value}; -use crate::structs::{image::Image, status::Status}; +use crate::structs::{status::Status, update::Update}; /// Helper function to get metrics used in JSON output -pub fn get_metrics(updates: &[Image]) -> Value { +pub fn get_metrics(updates: &[Update]) -> Value { let mut up_to_date = 0; let mut major_updates = 0; let mut minor_updates = 0; @@ -13,7 +13,7 @@ pub fn get_metrics(updates: &[Image]) -> Value { let mut other_updates = 0; let mut unknown = 0; updates.iter().for_each(|image| { - let has_update = image.has_update(); + let has_update = image.get_status(); match has_update { Status::UpdateMajor => { major_updates += 1; @@ -37,36 +37,39 @@ pub fn get_metrics(updates: &[Image]) -> Value { }); json!({ "monitored_images": updates.len(), - "up_to_date": up_to_date, "updates_available": major_updates + minor_updates + patch_updates + other_updates, "major_updates": major_updates, "minor_updates": minor_updates, "patch_updates": patch_updates, "other_updates": other_updates, + "up_to_date": up_to_date, "unknown": unknown }) } /// Takes a slice of `Image` objects and returns a `Value` with update info. The output doesn't contain much detail -pub fn to_simple_json(updates: &[Image]) -> Value { - let mut images = Map::new(); - updates.iter().for_each(|image| { - let _ = images.insert( - image.reference.clone(), - image.has_update().to_option_bool().into(), +pub fn to_simple_json(updates: &[Update]) -> Value { + let mut update_map = Map::new(); + updates.iter().for_each(|update| { + let _ = update_map.insert( + update.reference.clone(), + match update.result.has_update { + Some(has_update) => Value::Bool(has_update), + None => Value::Null, + }, ); }); let json_data: Value = json!({ "metrics": get_metrics(updates), - "images": images, + "images": updates, }); json_data } /// Takes a slice of `Image` objects and returns a `Value` with update info. All image data is included, useful for debugging. -pub fn to_full_json(updates: &[Image]) -> Value { +pub fn to_full_json(updates: &[Update]) -> Value { json!({ "metrics": get_metrics(updates), - "images": updates.iter().map(|image| image.to_json()).collect::>(), + "images": updates.iter().map(|update| serde_json::to_value(update).unwrap()).collect::>(), }) } diff --git a/src/utils/sort_update_vec.rs b/src/utils/sort_update_vec.rs index 311a909..599117e 100644 --- a/src/utils/sort_update_vec.rs +++ b/src/utils/sort_update_vec.rs @@ -1,12 +1,12 @@ use std::cmp::Ordering; -use crate::structs::image::Image; +use crate::structs::update::Update; /// Sorts the update vector alphabetically and by Status -pub fn sort_image_vec(updates: &[Image]) -> Vec { +pub fn sort_update_vec(updates: &[Update]) -> Vec { let mut sorted_updates = updates.to_vec(); sorted_updates.sort_by(|a, b| { - let cmp = a.has_update().cmp(&b.has_update()); + let cmp = a.get_status().cmp(&b.get_status()); if cmp == Ordering::Equal { a.reference.cmp(&b.reference) } else { @@ -18,10 +18,7 @@ pub fn sort_image_vec(updates: &[Image]) -> Vec { #[cfg(test)] mod tests { - use crate::structs::{ - image::{DigestInfo, VersionInfo}, - version::Version, - }; + use crate::structs::{status::Status, update::UpdateResult}; use super::*; @@ -40,8 +37,8 @@ mod tests { let digest_update_2 = create_digest_update("library/alpine"); let up_to_date_1 = create_up_to_date("docker:dind"); let up_to_date_2 = create_up_to_date("ghcr.io/sergi0g/cup"); - let unknown_1 = create_unknown("fake_registry.com/fake/image"); - let unknown_2 = create_unknown("private_registry.io/private/image"); + let unknown_1 = create_unknown("fake_registry.com/fake/Update"); + let unknown_2 = create_unknown("private_registry.io/private/Update"); let input_vec = vec![ major_update_2.clone(), unknown_2.clone(), @@ -72,102 +69,61 @@ mod tests { ]; // Sort the vec - let sorted_vec = sort_image_vec(&input_vec); + let sorted_vec = sort_update_vec(&input_vec); // Check results assert_eq!(sorted_vec, expected_vec); } - fn create_unknown(reference: &str) -> Image { - Image { + fn create_unknown(reference: &str) -> Update { + Update { reference: reference.to_string(), - error: Some("whoops".to_string()), + status: Status::Unknown("".to_string()), + result: UpdateResult { + has_update: None, + info: Default::default(), + error: Some("Error".to_string()), + }, ..Default::default() } } - fn create_up_to_date(reference: &str) -> Image { - Image { + fn create_up_to_date(reference: &str) -> Update { + Update { reference: reference.to_string(), - digest_info: Some(DigestInfo { - local_digests: vec![ - "some_digest".to_string(), - "some_other_digest".to_string(), - "latest_digest".to_string(), - ], - remote_digest: Some("latest_digest".to_string()), - }), + status: Status::UpToDate, ..Default::default() } } - fn create_digest_update(reference: &str) -> Image { - Image { + fn create_digest_update(reference: &str) -> Update { + Update { reference: reference.to_string(), - digest_info: Some(DigestInfo { - local_digests: vec!["some_digest".to_string(), "some_other_digest".to_string()], - remote_digest: Some("latest_digest".to_string()), - }), + status: Status::UpdateAvailable, ..Default::default() } } - fn create_patch_update(reference: &str) -> Image { - Image { + fn create_patch_update(reference: &str) -> Update { + Update { reference: reference.to_string(), - version_info: Some(VersionInfo { - current_tag: Version { - major: 19, - minor: Some(42), - patch: Some(999), - }, - latest_remote_tag: Some(Version { - major: 19, - minor: Some(42), - patch: Some(1000), - }), - format_str: String::new() - }), + status: Status::UpdatePatch, ..Default::default() } } - fn create_minor_update(reference: &str) -> Image { - Image { + fn create_minor_update(reference: &str) -> Update { + Update { reference: reference.to_string(), - version_info: Some(VersionInfo { - current_tag: Version { - major: 19, - minor: Some(42), - patch: Some(45), - }, - latest_remote_tag: Some(Version { - major: 19, - minor: Some(47), - patch: Some(2), - }), - format_str: String::new() - }), + status: Status::UpdateMinor, ..Default::default() } } - fn create_major_update(reference: &str) -> Image { - Image { + fn create_major_update(reference: &str) -> Update { + Update { reference: reference.to_string(), - version_info: Some(VersionInfo { - current_tag: Version { - major: 17, - minor: Some(42), - patch: None, - }, - latest_remote_tag: Some(Version { - major: 19, - minor: Some(0), - patch: None, - }), - format_str: String::new() - }), + status: Status::UpdateMajor, ..Default::default() } } diff --git a/web/src/App.tsx b/web/src/App.tsx index 4b56851..0c0fd9a 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -9,6 +9,17 @@ import { theme } from "./theme"; import RefreshButton from "./components/RefreshButton"; import Search from "./components/Search"; +const SORT_ORDER = [ + "monitored_images", + "updates_available", + "major_updates", + "minor_updates", + "patch_updates", + "other_updates", + "up_to_date", + "unknown", +]; + function App() { const [data, setData] = useState(null); const [searchQuery, setSearchQuery] = useState(""); @@ -29,7 +40,9 @@ function App() { className={`bg-white shadow-sm dark:bg-${theme}-900 my-8 rounded-md`} >
- {Object.entries(data.metrics).map(([name]) => ( + {Object.entries(data.metrics).sort((a, b) => { + return SORT_ORDER.indexOf(a[0]) - SORT_ORDER.indexOf(b[0]); + }).map(([name]) => (