mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-17 01:23:39 -05:00
Changed how updates are managed after checking (preparing for the new API)
This commit is contained in:
13
src/check.rs
13
src/check.rs
@@ -26,7 +26,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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(images: &[Image], config: &Config) -> Vec<(String, Option<bool>)> {
|
pub async fn get_updates(images: &[Image], config: &Config) -> Vec<Image> {
|
||||||
// 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()
|
||||||
@@ -80,16 +80,5 @@ pub async fn get_updates(images: &[Image], config: &Config) -> Vec<(String, Opti
|
|||||||
// Await all the futures
|
// Await all the futures
|
||||||
let final_images = join_all(handles).await;
|
let final_images = join_all(handles).await;
|
||||||
|
|
||||||
let mut result: Vec<(String, Option<bool>)> = Vec::with_capacity(images.len());
|
|
||||||
final_images
|
final_images
|
||||||
.iter()
|
|
||||||
.for_each(|image| match &image.remote_digest {
|
|
||||||
Some(digest) => {
|
|
||||||
let has_update = !image.local_digests.as_ref().unwrap().contains(digest);
|
|
||||||
result.push((image.reference.clone(), Some(has_update)))
|
|
||||||
}
|
|
||||||
None => result.push((image.reference.clone(), None)),
|
|
||||||
});
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,44 +2,41 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
|
||||||
use crate::utils::{sort_update_vec, to_json};
|
use crate::{image::{Image, Status}, utils::{sort_image_vec, to_simple_json}};
|
||||||
|
|
||||||
pub fn print_updates(updates: &[(String, Option<bool>)], icons: &bool) {
|
pub fn print_updates(updates: &[Image], icons: &bool) {
|
||||||
let sorted_updates = sort_update_vec(updates);
|
let sorted_images = sort_image_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 update in sorted_updates {
|
for image in sorted_images {
|
||||||
let description = match update.1 {
|
let has_update = image.has_update();
|
||||||
Some(true) => "Update available",
|
let description = has_update.to_string();
|
||||||
Some(false) => "Up to date",
|
|
||||||
None => "Unknown",
|
|
||||||
};
|
|
||||||
let icon = if *icons {
|
let icon = if *icons {
|
||||||
match update.1 {
|
match has_update {
|
||||||
Some(true) => "\u{f0aa} ",
|
Status::UpdateAvailable => "\u{f0aa} ",
|
||||||
Some(false) => "\u{f058} ",
|
Status::UpToDate => "\u{f058} ",
|
||||||
None => "\u{f059} ",
|
Status::Unknown(_) => "\u{f059} ",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
let color = match update.1 {
|
let color = match has_update {
|
||||||
Some(true) => "\u{001b}[38;5;12m",
|
Status::UpdateAvailable => "\u{001b}[38;5;12m",
|
||||||
Some(false) => "\u{001b}[38;5;2m",
|
Status::UpToDate => "\u{001b}[38;5;2m",
|
||||||
None => "\u{001b}[38;5;8m",
|
Status::Unknown(_) => "\u{001b}[38;5;8m",
|
||||||
};
|
};
|
||||||
let dynamic_space =
|
let dynamic_space =
|
||||||
" ".repeat(term_width - description.len() - icon.len() - update.0.len());
|
" ".repeat(term_width - description.len() - icon.len() - image.reference.len());
|
||||||
println!(
|
println!(
|
||||||
"{}{}{}{}{}\u{001b}[0m",
|
"{}{}{}{}{}\u{001b}[0m",
|
||||||
color, icon, update.0, dynamic_space, description
|
color, icon, image.reference, dynamic_space, description
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print_raw_updates(updates: &[(String, Option<bool>)]) {
|
pub fn print_raw_updates(updates: &[Image]) {
|
||||||
println!("{}", json::stringify(to_json(updates)));
|
println!("{}", json::stringify(to_simple_json(updates)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Spinner {
|
pub struct Spinner {
|
||||||
|
|||||||
66
src/image.rs
66
src/image.rs
@@ -6,7 +6,7 @@ use crate::error;
|
|||||||
|
|
||||||
/// Image struct that contains all information that may be needed by a function.
|
/// Image struct that contains all information that may be needed by a function.
|
||||||
/// It's designed to be passed around between functions
|
/// It's designed to be passed around between functions
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Image {
|
pub struct Image {
|
||||||
pub reference: String,
|
pub reference: String,
|
||||||
pub registry: Option<String>,
|
pub registry: Option<String>,
|
||||||
@@ -14,6 +14,7 @@ pub struct Image {
|
|||||||
pub tag: Option<String>,
|
pub tag: Option<String>,
|
||||||
pub local_digests: Option<Vec<String>>,
|
pub local_digests: Option<Vec<String>>,
|
||||||
pub remote_digest: Option<String>,
|
pub remote_digest: Option<String>,
|
||||||
|
pub error: Option<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
@@ -22,9 +23,6 @@ impl Image {
|
|||||||
if !image.repo_tags.is_empty() && !image.repo_digests.is_empty() {
|
if !image.repo_tags.is_empty() && !image.repo_digests.is_empty() {
|
||||||
let mut image = Image {
|
let mut image = Image {
|
||||||
reference: image.repo_tags[0].clone(),
|
reference: image.repo_tags[0].clone(),
|
||||||
registry: None,
|
|
||||||
repository: None,
|
|
||||||
tag: None,
|
|
||||||
local_digests: Some(
|
local_digests: Some(
|
||||||
image
|
image
|
||||||
.repo_digests
|
.repo_digests
|
||||||
@@ -33,7 +31,7 @@ impl Image {
|
|||||||
.map(|digest| digest.split('@').collect::<Vec<&str>>()[1].to_string())
|
.map(|digest| digest.split('@').collect::<Vec<&str>>()[1].to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
remote_digest: None,
|
..Default::default()
|
||||||
};
|
};
|
||||||
let (registry, repository, tag) = image.split();
|
let (registry, repository, tag) = image.split();
|
||||||
image.registry = Some(registry);
|
image.registry = Some(registry);
|
||||||
@@ -53,9 +51,6 @@ impl Image {
|
|||||||
{
|
{
|
||||||
let mut image = Image {
|
let mut image = Image {
|
||||||
reference: image.repo_tags.as_ref().unwrap()[0].clone(),
|
reference: image.repo_tags.as_ref().unwrap()[0].clone(),
|
||||||
registry: None,
|
|
||||||
repository: None,
|
|
||||||
tag: None,
|
|
||||||
local_digests: Some(
|
local_digests: Some(
|
||||||
image
|
image
|
||||||
.repo_digests
|
.repo_digests
|
||||||
@@ -65,7 +60,7 @@ impl Image {
|
|||||||
.map(|digest| digest.split('@').collect::<Vec<&str>>()[1].to_string())
|
.map(|digest| digest.split('@').collect::<Vec<&str>>()[1].to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
remote_digest: None,
|
..Default::default()
|
||||||
};
|
};
|
||||||
let (registry, repository, tag) = image.split();
|
let (registry, repository, tag) = image.split();
|
||||||
image.registry = Some(registry);
|
image.registry = Some(registry);
|
||||||
@@ -108,6 +103,31 @@ impl Image {
|
|||||||
None => error!("Failed to parse image {}", &self.reference),
|
None => error!("Failed to parse image {}", &self.reference),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compares remote digest of the image with its local digests to determine if it has an update or not
|
||||||
|
pub fn has_update(&self) -> Status {
|
||||||
|
if self.error.is_some() {
|
||||||
|
Status::Unknown(self.error.clone().unwrap())
|
||||||
|
} else if self.local_digests.as_ref().unwrap().contains(&self.remote_digest.as_ref().unwrap()) {
|
||||||
|
Status::UpToDate
|
||||||
|
} else {
|
||||||
|
Status::UpdateAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Image {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
reference: String::new(),
|
||||||
|
registry: None,
|
||||||
|
repository: None,
|
||||||
|
tag: None,
|
||||||
|
local_digests: None,
|
||||||
|
remote_digest: None,
|
||||||
|
error: None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Regex to match Docker image references against, so registry, repository and tag can be extracted.
|
/// Regex to match Docker image references against, so registry, repository and tag can be extracted.
|
||||||
@@ -117,3 +137,31 @@ static RE: Lazy<Regex> = Lazy::new(|| {
|
|||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Enum for image status
|
||||||
|
pub enum Status {
|
||||||
|
UpToDate,
|
||||||
|
UpdateAvailable,
|
||||||
|
Unknown(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for Status {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match &self {
|
||||||
|
Self::UpToDate => "Up to date",
|
||||||
|
Self::UpdateAvailable => "Update available",
|
||||||
|
Self::Unknown(_) => "Unknown"
|
||||||
|
}.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Status {
|
||||||
|
// Converts the Status into an Option<bool> (useful for JSON serialization)
|
||||||
|
pub fn to_option_bool(&self) -> Option<bool> {
|
||||||
|
match &self {
|
||||||
|
Self::UpdateAvailable => Some(true),
|
||||||
|
Self::UpToDate => Some(false),
|
||||||
|
Self::Unknown(_) => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,14 +82,15 @@ pub async fn get_latest_digest(
|
|||||||
let status = response.status();
|
let status = response.status();
|
||||||
if status == 401 {
|
if status == 401 {
|
||||||
if token.is_some() {
|
if token.is_some() {
|
||||||
warn!("Failed to authenticate to registry {} with given token!\n{}", &image.registry.as_ref().unwrap(), token.unwrap());
|
warn!("Failed to authenticate to registry {} with token provided!\n{}", &image.registry.as_ref().unwrap(), token.unwrap());
|
||||||
|
return Image { remote_digest: None, error: Some(format!("Authentication token \"{}\" was not accepted", token.unwrap())), ..image.clone() }
|
||||||
} else {
|
} else {
|
||||||
warn!("Registry requires authentication");
|
warn!("Registry requires authentication");
|
||||||
|
return Image { remote_digest: None, error: Some("Registry requires authentication".to_string()), ..image.clone() }
|
||||||
}
|
}
|
||||||
return Image { remote_digest: None, ..image.clone() }
|
|
||||||
} else if status == 404 {
|
} else if status == 404 {
|
||||||
warn!("Image {:?} not found", &image);
|
warn!("Image {:?} not found", &image);
|
||||||
return Image { remote_digest: None, ..image.clone() }
|
return Image { remote_digest: None, error: Some("Image not found".to_string()), ..image.clone() }
|
||||||
} else {
|
} else {
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
@@ -97,7 +98,7 @@ pub async fn get_latest_digest(
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.is_connect() {
|
if e.is_connect() {
|
||||||
warn!("Connection to registry failed.");
|
warn!("Connection to registry failed.");
|
||||||
return Image { remote_digest: None, ..image.clone() }
|
return Image { remote_digest: None, error: Some("Connection to registry failed".to_string()), ..image.clone() }
|
||||||
} else {
|
} else {
|
||||||
error!("Unexpected error: {}", e.to_string())
|
error!("Unexpected error: {}", e.to_string())
|
||||||
}
|
}
|
||||||
@@ -140,7 +141,7 @@ pub async fn get_token(
|
|||||||
Ok(response) => match response.text().await {
|
Ok(response) => match response.text().await {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to parse response into string!\n{}", e)
|
error!("Failed to parse registry response into string!\n{}", e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use json::JsonValue;
|
use json::JsonValue;
|
||||||
use liquid::{object, Object};
|
use liquid::{object, Object, ValueView};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use xitca_web::{
|
use xitca_web::{
|
||||||
body::ResponseBody,
|
body::ResponseBody,
|
||||||
@@ -14,7 +14,7 @@ use xitca_web::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
check::get_updates, config::{Config, Theme}, docker::get_images_from_docker_daemon, info, utils::{sort_update_vec, to_json}
|
check::get_updates, config::{Config, Theme}, docker::get_images_from_docker_daemon, image::Image, info, utils::{sort_image_vec, to_simple_json}
|
||||||
};
|
};
|
||||||
|
|
||||||
const HTML: &str = include_str!("static/index.html");
|
const HTML: &str = include_str!("static/index.html");
|
||||||
@@ -90,7 +90,7 @@ async fn refresh(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
|
|||||||
|
|
||||||
struct ServerData {
|
struct ServerData {
|
||||||
template: String,
|
template: String,
|
||||||
raw_updates: Vec<(String, Option<bool>)>,
|
raw_updates: Vec<Image>,
|
||||||
json: JsonValue,
|
json: JsonValue,
|
||||||
config: Config,
|
config: Config,
|
||||||
theme: &'static str,
|
theme: &'static str,
|
||||||
@@ -117,7 +117,7 @@ impl ServerData {
|
|||||||
info!("Refreshing data");
|
info!("Refreshing data");
|
||||||
}
|
}
|
||||||
let images = get_images_from_docker_daemon(&self.config, &None).await;
|
let images = get_images_from_docker_daemon(&self.config, &None).await;
|
||||||
let updates = sort_update_vec(&get_updates(&images, &self.config).await);
|
let updates = sort_image_vec(&get_updates(&images, &self.config).await);
|
||||||
let end = Local::now().timestamp_millis();
|
let end = Local::now().timestamp_millis();
|
||||||
info!("✨ Checked {} images in {}ms", updates.len(), end - start);
|
info!("✨ Checked {} images in {}ms", updates.len(), end - start);
|
||||||
self.raw_updates = updates;
|
self.raw_updates = updates;
|
||||||
@@ -129,12 +129,9 @@ impl ServerData {
|
|||||||
let images = self
|
let images = self
|
||||||
.raw_updates
|
.raw_updates
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(name, has_update)| match has_update {
|
.map(|image| object!({"name": image.reference, "has_update": image.has_update().to_option_bool().to_value()}),)
|
||||||
Some(v) => object!({"name": name, "has_update": v.to_string()}), // Liquid kinda thinks false == nil, so we'll be comparing strings from now on
|
|
||||||
None => object!({"name": name, "has_update": "null"}),
|
|
||||||
})
|
|
||||||
.collect::<Vec<Object>>();
|
.collect::<Vec<Object>>();
|
||||||
self.json = to_json(&self.raw_updates);
|
self.json = to_simple_json(&self.raw_updates);
|
||||||
let last_updated = Local::now();
|
let last_updated = Local::now();
|
||||||
self.json["last_updated"] = last_updated
|
self.json["last_updated"] = last_updated
|
||||||
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
|
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
|
||||||
|
|||||||
111
src/utils.rs
111
src/utils.rs
@@ -2,41 +2,53 @@ use json::{object, JsonValue};
|
|||||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
||||||
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||||
|
|
||||||
|
use crate::image::{Image, Status};
|
||||||
|
|
||||||
/// Sorts the update vector alphabetically and where Some(true) > Some(false) > None
|
/// Sorts the update vector alphabetically and where Some(true) > Some(false) > None
|
||||||
pub fn sort_update_vec(updates: &[(String, Option<bool>)]) -> Vec<(String, Option<bool>)> {
|
pub fn sort_image_vec(updates: &[Image]) -> Vec<Image> {
|
||||||
let mut sorted_updates = updates.to_vec();
|
let mut sorted_updates = updates.to_vec();
|
||||||
sorted_updates.sort_unstable_by(|a, b| match (a.1, b.1) {
|
sorted_updates.sort_unstable_by(|a, b| match (a.has_update(), b.has_update()) {
|
||||||
(Some(c), Some(d)) => {
|
(Status::UpdateAvailable, Status::UpdateAvailable) => {
|
||||||
if c == d {
|
a.reference.cmp(&b.reference)
|
||||||
a.0.cmp(&b.0)
|
}
|
||||||
} else {
|
(Status::UpdateAvailable, Status::UpToDate | Status::Unknown(_)) => std::cmp::Ordering::Less,
|
||||||
(!c).cmp(&!d)
|
(Status::UpToDate, Status::UpdateAvailable) => std::cmp::Ordering::Greater,
|
||||||
}
|
(Status::UpToDate, Status::UpToDate) => {
|
||||||
|
a.reference.cmp(&b.reference)
|
||||||
|
},
|
||||||
|
(Status::UpToDate, Status::Unknown(_)) => std::cmp::Ordering::Less,
|
||||||
|
(Status::Unknown(_), Status::UpdateAvailable | Status::UpToDate) => std::cmp::Ordering::Greater,
|
||||||
|
(Status::Unknown(_), Status::Unknown(_)) => {
|
||||||
|
a.reference.cmp(&b.reference)
|
||||||
}
|
}
|
||||||
(Some(_), None) => std::cmp::Ordering::Less,
|
|
||||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
|
||||||
(None, None) => a.0.cmp(&b.0),
|
|
||||||
});
|
});
|
||||||
sorted_updates.to_vec()
|
sorted_updates.to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json(updates: &[(String, Option<bool>)]) -> JsonValue {
|
/// Takes a slice of `Image` objects and returns a `JsonValue` of update info. The output doesn't contain much detail
|
||||||
|
pub fn to_simple_json(updates: &[Image]) -> JsonValue {
|
||||||
let mut json_data: JsonValue = object! {
|
let mut json_data: JsonValue = object! {
|
||||||
metrics: object! {},
|
metrics: object! {},
|
||||||
images: object! {}
|
images: object! {}
|
||||||
};
|
};
|
||||||
updates.iter().for_each(|(image, has_update)| {
|
let mut up_to_date = 0;
|
||||||
let _ = json_data["images"].insert(image, *has_update);
|
let mut update_available = 0;
|
||||||
|
let mut unknown = 0;
|
||||||
|
updates.iter().for_each(|image| {
|
||||||
|
let has_update = image.has_update();
|
||||||
|
match has_update {
|
||||||
|
Status::UpdateAvailable => {
|
||||||
|
update_available += 1;
|
||||||
|
},
|
||||||
|
Status::UpToDate => {
|
||||||
|
up_to_date += 1;
|
||||||
|
},
|
||||||
|
Status::Unknown(_) => {
|
||||||
|
unknown += 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = json_data["images"].insert(&image.reference, has_update.to_option_bool());
|
||||||
});
|
});
|
||||||
let up_to_date = updates
|
|
||||||
.iter()
|
|
||||||
.filter(|&(_, value)| *value == Some(false))
|
|
||||||
.count();
|
|
||||||
let update_available = updates
|
|
||||||
.iter()
|
|
||||||
.filter(|&(_, value)| *value == Some(true))
|
|
||||||
.count();
|
|
||||||
let unknown = updates.iter().filter(|&(_, value)| value.is_none()).count();
|
|
||||||
let _ = json_data["metrics"].insert("monitored_images", updates.len());
|
let _ = json_data["metrics"].insert("monitored_images", updates.len());
|
||||||
let _ = json_data["metrics"].insert("up_to_date", up_to_date);
|
let _ = json_data["metrics"].insert("up_to_date", up_to_date);
|
||||||
let _ = json_data["metrics"].insert("update_available", update_available);
|
let _ = json_data["metrics"].insert("update_available", update_available);
|
||||||
@@ -84,3 +96,56 @@ pub fn new_reqwest_client() -> ClientWithMiddleware {
|
|||||||
))
|
))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Test the `sort_update_vec` function
|
||||||
|
#[test]
|
||||||
|
fn test_ordering() {
|
||||||
|
// Create test objects
|
||||||
|
let update_available_1 = Image {
|
||||||
|
reference: "busybox".to_string(),
|
||||||
|
local_digests: Some(vec!["some_digest".to_string(), "some_other_digest".to_string()]),
|
||||||
|
remote_digest: Some("latest_digest".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let update_available_2 = Image {
|
||||||
|
reference: "library/alpine".to_string(),
|
||||||
|
local_digests: Some(vec!["some_digest".to_string(), "some_other_digest".to_string()]), // We don't need to mock real data, as this is a generic function
|
||||||
|
remote_digest: Some("latest_digest".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let up_to_date_1 = Image {
|
||||||
|
reference: "docker:dind".to_string(),
|
||||||
|
local_digests: Some(vec!["some_digest".to_string(), "some_other_digest".to_string(), "latest_digest".to_string()]),
|
||||||
|
remote_digest: Some("latest_digest".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let up_to_date_2 = Image {
|
||||||
|
reference: "ghcr.io/sergi0g/cup".to_string(),
|
||||||
|
local_digests: Some(vec!["some_digest".to_string(), "some_other_digest".to_string(), "latest_digest".to_string()]),
|
||||||
|
remote_digest: Some("latest_digest".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let unknown_1 = Image {
|
||||||
|
reference: "fake_registry.com/fake/image".to_string(),
|
||||||
|
error: Some("whoops".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let unknown_2 = Image {
|
||||||
|
reference: "private_registry.io/private/image".to_string(),
|
||||||
|
error: Some("whoops".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let input_vec = vec![unknown_2.clone(), up_to_date_1.clone(), unknown_1.clone(), update_available_2.clone(), update_available_1.clone(), up_to_date_2.clone()];
|
||||||
|
let expected_vec = vec![update_available_1, update_available_2, up_to_date_1, up_to_date_2, unknown_1, unknown_2];
|
||||||
|
|
||||||
|
// Sort the vec
|
||||||
|
let sorted_vec = sort_image_vec(&input_vec);
|
||||||
|
|
||||||
|
// Check results
|
||||||
|
assert_eq!(sorted_vec, expected_vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user