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

Get updates from multiple servers (part one: get the data)

This commit is contained in:
Sergio
2025-01-02 20:11:31 +02:00
parent a1711b7ac8
commit aeeffaccba
13 changed files with 348 additions and 187 deletions

View File

@@ -8,44 +8,93 @@ use crate::{
docker::get_images_from_docker_daemon, docker::get_images_from_docker_daemon,
http::Client, http::Client,
registry::{check_auth, get_token}, 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<Update> {
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<Update> = 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. /// Returns a list of updates for all images passed in.
pub async fn get_updates(references: &Option<Vec<String>>, config: &Config) -> Vec<Image> { pub async fn get_updates(references: &Option<Vec<String>>, config: &Config) -> Vec<Update> {
// Get images let client = Client::new();
// Get local images
debug!(config.debug, "Retrieving images to be checked"); debug!(config.debug, "Retrieving images to be checked");
let mut images = get_images_from_docker_daemon(config, references).await; let mut images = get_images_from_docker_daemon(config, references).await;
let extra_images = match references {
Some(refs) => { // Add extra images from references
let image_refs: FxHashSet<&String> = if let Some(refs) = references {
images.iter().map(|image| &image.reference).collect(); let image_refs: FxHashSet<&String> =
let extra = refs images.iter().map(|image| &image.reference).collect();
.iter() let extra = refs
.filter(|&reference| !image_refs.contains(reference)) .iter()
.collect::<Vec<&String>>(); .filter(|&reference| !image_refs.contains(reference))
Some( .map(|reference| Image::from_reference(reference))
extra .collect::<Vec<Image>>();
.iter() images.extend(extra);
.map(|reference| Image::from_reference(reference))
.collect::<Vec<Image>>(),
)
}
None => None,
};
if let Some(extra_imgs) = extra_images {
images.extend_from_slice(&extra_imgs);
} }
// 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!( debug!(
config.debug, config.debug,
"Checking {:?}", "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. // 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 let registries: Vec<&String> = images
.iter() .iter()
.map(|image| &image.registry) .map(|image| &image.parts.registry)
.unique() .unique()
.collect::<Vec<&String>>(); .collect::<Vec<&String>>();
@@ -57,7 +106,7 @@ pub async fn get_updates(references: &Option<Vec<String>>, config: &Config) -> V
let mut image_map: FxHashMap<&String, Vec<&Image>> = FxHashMap::default(); let mut image_map: FxHashMap<&String, Vec<&Image>> = FxHashMap::default();
for image in &images { 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. // Retrieve an authentication token (if required) for each registry.
@@ -87,9 +136,6 @@ pub async fn get_updates(references: &Option<Vec<String>>, config: &Config) -> V
debug!(config.debug, "Tokens: {:?}", tokens); 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 let ignored_registries = config
.registries .registries
.iter() .iter()
@@ -102,15 +148,25 @@ pub async fn get_updates(references: &Option<Vec<String>>, config: &Config) -> V
}) })
.collect::<Vec<&String>>(); .collect::<Vec<&String>>();
// 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 { 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 { 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); let future = image.check(token.as_deref(), config, &client);
handles.push(future); handles.push(future);
} }
} }
// Await all the futures // Await all the futures
join_all(handles).await let images = join_all(handles).await;
let mut updates: Vec<Update> = images.iter().map(|image| image.to_update()).collect();
updates.extend_from_slice(&remote_updates);
updates
} }

View File

@@ -1,17 +1,17 @@
pub mod spinner; pub mod spinner;
use crate::{ use crate::{
structs::{image::Image, status::Status}, structs::{status::Status, update::Update},
utils::{json::to_simple_json, sort_update_vec::sort_image_vec}, utils::{json::to_simple_json, sort_update_vec::sort_update_vec},
}; };
pub fn print_updates(updates: &[Image], icons: &bool) { pub fn print_updates(updates: &[Update], icons: &bool) {
let sorted_images = sort_image_vec(updates); let sorted_images = sort_update_vec(updates);
let term_width: usize = termsize::get() let term_width: usize = termsize::get()
.unwrap_or(termsize::Size { rows: 24, cols: 80 }) .unwrap_or(termsize::Size { rows: 24, cols: 80 })
.cols as usize; .cols as usize;
for image in sorted_images { for image in sorted_images {
let has_update = image.has_update(); let has_update = image.get_status();
let description = has_update.to_string(); let description = has_update.to_string();
let icon = if *icons { let icon = if *icons {
match has_update { 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)); println!("{}", to_simple_json(updates));
} }

View File

@@ -52,10 +52,10 @@ pub async fn get_latest_digest(
"Checking for digest update to {}", image.reference "Checking for digest update to {}", image.reference
); );
let start = SystemTime::now(); let start = SystemTime::now();
let protocol = get_protocol(&image.registry, &config.registries); let protocol = get_protocol(&image.parts.registry, &config.registries);
let url = format!( let url = format!(
"{}://{}/v2/{}/manifests/{}", "{}://{}/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 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())]; 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 { ) -> String {
let mut url = auth_url.to_owned(); let mut url = auth_url.to_owned();
for image in images { 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 authorization = credentials.as_ref().map(|creds| format!("Basic {}", creds));
let headers = vec![("Authorization", authorization.as_deref())]; let headers = vec![("Authorization", authorization.as_deref())];
@@ -128,10 +128,10 @@ pub async fn get_latest_tag(
"Checking for tag update to {}", image.reference "Checking for tag update to {}", image.reference
); );
let start = now(); let start = now();
let protocol = get_protocol(&image.registry, &config.registries); let protocol = get_protocol(&image.parts.registry, &config.registries);
let url = format!( let url = format!(
"{}://{}/v2/{}/tags/list", "{}://{}/v2/{}/tags/list",
protocol, &image.registry, &image.repository, protocol, &image.parts.registry, &image.parts.repository,
); );
let authorization = to_bearer_string(&token); let authorization = to_bearer_string(&token);
let headers = vec![ let headers = vec![

View File

@@ -19,10 +19,10 @@ use crate::{
check::get_updates, check::get_updates,
config::{Config, Theme}, config::{Config, Theme},
info, info,
structs::image::Image, structs::update::Update,
utils::{ utils::{
json::{to_full_json, to_simple_json}, json::{to_full_json, to_simple_json},
sort_update_vec::sort_image_vec, sort_update_vec::sort_update_vec,
time::{elapsed, now}, time::{elapsed, now},
}, },
}; };
@@ -147,7 +147,7 @@ async fn refresh(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
struct ServerData { struct ServerData {
template: String, template: String,
raw_updates: Vec<Image>, raw_updates: Vec<Update>,
simple_json: Value, simple_json: Value,
full_json: Value, full_json: Value,
config: Config, config: Config,
@@ -172,7 +172,7 @@ impl ServerData {
if !self.raw_updates.is_empty() { if !self.raw_updates.is_empty() {
info!("Refreshing data"); 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!( info!(
"✨ Checked {} images in {}ms", "✨ Checked {} images in {}ms",
updates.len(), updates.len(),
@@ -187,7 +187,7 @@ impl ServerData {
let images = self let images = self
.raw_updates .raw_updates
.iter() .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::<Vec<Object>>(); .collect::<Vec<Object>>();
self.simple_json = to_simple_json(&self.raw_updates); self.simple_json = to_simple_json(&self.raw_updates);
self.full_json = to_full_json(&self.raw_updates); self.full_json = to_full_json(&self.raw_updates);

View File

@@ -1,5 +1,3 @@
use serde_json::{json, Value};
use crate::{ use crate::{
config::Config, config::Config,
error, error,
@@ -9,7 +7,11 @@ use crate::{
utils::reference::split, utils::reference::split,
}; };
use super::inspectdata::InspectData; use super::{
inspectdata::InspectData,
parts::Parts,
update::{DigestUpdateInfo, Update, UpdateInfo, UpdateResult, VersionUpdateInfo},
};
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
#[cfg_attr(test, derive(Debug))] #[cfg_attr(test, derive(Debug))]
@@ -23,7 +25,7 @@ pub struct DigestInfo {
pub struct VersionInfo { pub struct VersionInfo {
pub current_tag: Version, pub current_tag: Version,
pub latest_remote_tag: Option<Version>, pub latest_remote_tag: Option<Version>,
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. /// 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))] #[cfg_attr(test, derive(Debug))]
pub struct Image { pub struct Image {
pub reference: String, pub reference: String,
pub registry: String, pub parts: Parts,
pub repository: String,
pub tag: String,
pub digest_info: Option<DigestInfo>, pub digest_info: Option<DigestInfo>,
pub version_info: Option<VersionInfo>, pub version_info: Option<VersionInfo>,
pub error: Option<String>, pub error: Option<String>,
@@ -56,9 +56,11 @@ impl Image {
.collect(); .collect();
Some(Self { Some(Self {
reference, reference,
registry, parts: Parts {
repository, registry,
tag, repository,
tag,
},
digest_info: Some(DigestInfo { digest_info: Some(DigestInfo {
local_digests, local_digests,
remote_digest: None, remote_digest: None,
@@ -82,9 +84,11 @@ impl Image {
match version_tag { match version_tag {
Some((version, format_str)) => Self { Some((version, format_str)) => Self {
reference: reference.to_string(), reference: reference.to_string(),
registry, parts: Parts {
repository, registry,
tag, repository,
tag,
},
version_info: Some(VersionInfo { version_info: Some(VersionInfo {
current_tag: version, current_tag: version,
format_str, format_str,
@@ -127,59 +131,65 @@ impl Image {
} }
} }
/// Converts image data into a `Value` /// Converts image data into an `Update`
pub fn to_json(&self) -> Value { pub fn to_update(&self) -> Update {
let has_update = self.has_update(); let has_update = self.has_update();
let update_type = match has_update { let update_type = match has_update {
Status::UpdateMajor | Status::UpdateMinor | Status::UpdatePatch => "version", Status::UpdateMajor | Status::UpdateMinor | Status::UpdatePatch => "version",
_ => "digest", _ => "digest",
}; };
json!({ Update {
"reference": self.reference.clone(), reference: self.reference.clone(),
"parts": { parts: self.parts.clone(),
"registry": self.registry.clone(), result: UpdateResult {
"repository": self.repository.clone(), has_update: has_update.to_option_bool(),
"tag": self.tag.clone() info: match has_update {
}, Status::Unknown(_) => UpdateInfo::None,
"result": { _ => match update_type {
"has_update": has_update.to_option_bool(),
"info": match has_update {
Status::Unknown(_) => None,
_ => Some(match update_type {
"version" => { "version" => {
let (version_tag, latest_remote_tag) = match &self.version_info { let (new_tag, format_str) = match &self.version_info {
Some(data) => (data.current_tag.clone(), data.latest_remote_tag.clone()), Some(data) => (
_ => unreachable!() data.latest_remote_tag.clone().unwrap(),
data.format_str.clone(),
),
_ => unreachable!(),
}; };
json!({
"type": update_type, UpdateInfo::Version(VersionUpdateInfo {
"version_update_type": match has_update { version_update_type: match has_update {
Status::UpdateMajor => "major", Status::UpdateMajor => "major",
Status::UpdateMinor => "minor", Status::UpdateMinor => "minor",
Status::UpdatePatch => "patch", Status::UpdatePatch => "patch",
_ => unreachable!() _ => unreachable!(),
}, }
"new_version": self.tag.replace(&version_tag.to_string(), &latest_remote_tag.as_ref().unwrap().to_string()) .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" => { "digest" => {
let (local_digests, remote_digest) = match &self.digest_info { let (local_digests, remote_digest) = match &self.digest_info {
Some(data) => (data.local_digests.clone(), data.remote_digest.clone()), Some(data) => {
_ => unreachable!() (data.local_digests.clone(), data.remote_digest.clone())
}
_ => unreachable!(),
}; };
json!({ UpdateInfo::Digest(DigestUpdateInfo {
"type": update_type, local_digests,
"local_digests": local_digests, remote_digest,
"remote_digest": 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 /// Checks if the image has an update

View File

@@ -2,3 +2,5 @@ pub mod image;
pub mod inspectdata; pub mod inspectdata;
pub mod status; pub mod status;
pub mod version; pub mod version;
pub mod update;
pub mod parts;

8
src/structs/parts.rs Normal file
View File

@@ -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,
}

View File

@@ -1,7 +1,8 @@
use std::fmt::Display; use std::fmt::Display;
/// Enum for image status /// Enum for image status
#[derive(Ord, Eq, PartialEq, PartialOrd)] #[derive(Ord, Eq, PartialEq, PartialOrd, Clone)]
#[cfg_attr(test, derive(Debug))]
pub enum Status { pub enum Status {
UpdateMajor, UpdateMajor,
UpdateMinor, UpdateMinor,
@@ -34,3 +35,9 @@ impl Status {
} }
} }
} }
impl Default for Status {
fn default() -> Self {
Self::Unknown("".to_string())
}
}

106
src/structs/update.rs Normal file
View File

@@ -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<String>,
#[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<bool>,
pub info: UpdateInfo,
pub error: Option<String>,
}
#[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<String>,
pub remote_digest: Option<String>,
}
impl Serialize for VersionUpdateInfo {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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()
}
}
}

View File

@@ -2,10 +2,10 @@
use serde_json::{json, Map, Value}; 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 /// 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 up_to_date = 0;
let mut major_updates = 0; let mut major_updates = 0;
let mut minor_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 other_updates = 0;
let mut unknown = 0; let mut unknown = 0;
updates.iter().for_each(|image| { updates.iter().for_each(|image| {
let has_update = image.has_update(); let has_update = image.get_status();
match has_update { match has_update {
Status::UpdateMajor => { Status::UpdateMajor => {
major_updates += 1; major_updates += 1;
@@ -37,36 +37,39 @@ pub fn get_metrics(updates: &[Image]) -> Value {
}); });
json!({ json!({
"monitored_images": updates.len(), "monitored_images": updates.len(),
"up_to_date": up_to_date,
"updates_available": major_updates + minor_updates + patch_updates + other_updates, "updates_available": major_updates + minor_updates + patch_updates + other_updates,
"major_updates": major_updates, "major_updates": major_updates,
"minor_updates": minor_updates, "minor_updates": minor_updates,
"patch_updates": patch_updates, "patch_updates": patch_updates,
"other_updates": other_updates, "other_updates": other_updates,
"up_to_date": up_to_date,
"unknown": unknown "unknown": unknown
}) })
} }
/// Takes a slice of `Image` objects and returns a `Value` with update info. The output doesn't contain much detail /// 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 { pub fn to_simple_json(updates: &[Update]) -> Value {
let mut images = Map::new(); let mut update_map = Map::new();
updates.iter().for_each(|image| { updates.iter().for_each(|update| {
let _ = images.insert( let _ = update_map.insert(
image.reference.clone(), update.reference.clone(),
image.has_update().to_option_bool().into(), match update.result.has_update {
Some(has_update) => Value::Bool(has_update),
None => Value::Null,
},
); );
}); });
let json_data: Value = json!({ let json_data: Value = json!({
"metrics": get_metrics(updates), "metrics": get_metrics(updates),
"images": images, "images": updates,
}); });
json_data json_data
} }
/// Takes a slice of `Image` objects and returns a `Value` with update info. All image data is included, useful for debugging. /// 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!({ json!({
"metrics": get_metrics(updates), "metrics": get_metrics(updates),
"images": updates.iter().map(|image| image.to_json()).collect::<Vec<Value>>(), "images": updates.iter().map(|update| serde_json::to_value(update).unwrap()).collect::<Vec<Value>>(),
}) })
} }

View File

@@ -1,12 +1,12 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use crate::structs::image::Image; use crate::structs::update::Update;
/// Sorts the update vector alphabetically and by Status /// Sorts the update vector alphabetically and by Status
pub fn sort_image_vec(updates: &[Image]) -> Vec<Image> { pub fn sort_update_vec(updates: &[Update]) -> Vec<Update> {
let mut sorted_updates = updates.to_vec(); let mut sorted_updates = updates.to_vec();
sorted_updates.sort_by(|a, b| { 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 { if cmp == Ordering::Equal {
a.reference.cmp(&b.reference) a.reference.cmp(&b.reference)
} else { } else {
@@ -18,10 +18,7 @@ pub fn sort_image_vec(updates: &[Image]) -> Vec<Image> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::structs::{ use crate::structs::{status::Status, update::UpdateResult};
image::{DigestInfo, VersionInfo},
version::Version,
};
use super::*; use super::*;
@@ -40,8 +37,8 @@ mod tests {
let digest_update_2 = create_digest_update("library/alpine"); let digest_update_2 = create_digest_update("library/alpine");
let up_to_date_1 = create_up_to_date("docker:dind"); 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 up_to_date_2 = create_up_to_date("ghcr.io/sergi0g/cup");
let unknown_1 = create_unknown("fake_registry.com/fake/image"); let unknown_1 = create_unknown("fake_registry.com/fake/Update");
let unknown_2 = create_unknown("private_registry.io/private/image"); let unknown_2 = create_unknown("private_registry.io/private/Update");
let input_vec = vec![ let input_vec = vec![
major_update_2.clone(), major_update_2.clone(),
unknown_2.clone(), unknown_2.clone(),
@@ -72,102 +69,61 @@ mod tests {
]; ];
// Sort the vec // Sort the vec
let sorted_vec = sort_image_vec(&input_vec); let sorted_vec = sort_update_vec(&input_vec);
// Check results // Check results
assert_eq!(sorted_vec, expected_vec); assert_eq!(sorted_vec, expected_vec);
} }
fn create_unknown(reference: &str) -> Image { fn create_unknown(reference: &str) -> Update {
Image { Update {
reference: reference.to_string(), 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() ..Default::default()
} }
} }
fn create_up_to_date(reference: &str) -> Image { fn create_up_to_date(reference: &str) -> Update {
Image { Update {
reference: reference.to_string(), reference: reference.to_string(),
digest_info: Some(DigestInfo { status: Status::UpToDate,
local_digests: vec![
"some_digest".to_string(),
"some_other_digest".to_string(),
"latest_digest".to_string(),
],
remote_digest: Some("latest_digest".to_string()),
}),
..Default::default() ..Default::default()
} }
} }
fn create_digest_update(reference: &str) -> Image { fn create_digest_update(reference: &str) -> Update {
Image { Update {
reference: reference.to_string(), reference: reference.to_string(),
digest_info: Some(DigestInfo { status: Status::UpdateAvailable,
local_digests: vec!["some_digest".to_string(), "some_other_digest".to_string()],
remote_digest: Some("latest_digest".to_string()),
}),
..Default::default() ..Default::default()
} }
} }
fn create_patch_update(reference: &str) -> Image { fn create_patch_update(reference: &str) -> Update {
Image { Update {
reference: reference.to_string(), reference: reference.to_string(),
version_info: Some(VersionInfo { status: Status::UpdatePatch,
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()
}),
..Default::default() ..Default::default()
} }
} }
fn create_minor_update(reference: &str) -> Image { fn create_minor_update(reference: &str) -> Update {
Image { Update {
reference: reference.to_string(), reference: reference.to_string(),
version_info: Some(VersionInfo { status: Status::UpdateMinor,
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()
}),
..Default::default() ..Default::default()
} }
} }
fn create_major_update(reference: &str) -> Image { fn create_major_update(reference: &str) -> Update {
Image { Update {
reference: reference.to_string(), reference: reference.to_string(),
version_info: Some(VersionInfo { status: Status::UpdateMajor,
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()
}),
..Default::default() ..Default::default()
} }
} }

View File

@@ -9,6 +9,17 @@ import { theme } from "./theme";
import RefreshButton from "./components/RefreshButton"; import RefreshButton from "./components/RefreshButton";
import Search from "./components/Search"; 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() { function App() {
const [data, setData] = useState<Data | null>(null); const [data, setData] = useState<Data | null>(null);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@@ -29,7 +40,9 @@ function App() {
className={`bg-white shadow-sm dark:bg-${theme}-900 my-8 rounded-md`} className={`bg-white shadow-sm dark:bg-${theme}-900 my-8 rounded-md`}
> >
<dl className="grid grid-cols-2 gap-1 overflow-hidden *:relative lg:grid-cols-4"> <dl className="grid grid-cols-2 gap-1 overflow-hidden *:relative lg:grid-cols-4">
{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]) => (
<Statistic <Statistic
name={name as keyof typeof data.metrics} name={name as keyof typeof data.metrics}
metrics={data.metrics} metrics={data.metrics}

View File

@@ -36,7 +36,7 @@ export default function Image({ data }: { data: Image }) {
}; };
const new_reference = const new_reference =
data.result.info?.type == "version" data.result.info?.type == "version"
? data.reference.replace(data.parts.tag, data.result.info.new_version) ? data.reference.split(":")[0] + ":" + data.result.info.new_version
: data.reference; : data.reference;
var url: string | null = null; var url: string | null = null;
if (clickable_registries.includes(data.parts.registry)) { if (clickable_registries.includes(data.parts.registry)) {