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:
120
src/check.rs
120
src/check.rs
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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![
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
8
src/structs/parts.rs
Normal 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,
|
||||||
|
}
|
||||||
@@ -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
106
src/structs/update.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>>(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user