m/cup
1
0
mirror of https://github.com/sergi0g/cup.git synced 2025-11-17 09:33:38 -05:00

Added new full json API route and changed API routes

This commit is contained in:
Sergio
2024-10-25 12:12:59 +03:00
parent 8ab073d562
commit 8fd012efbe
9 changed files with 174 additions and 85 deletions

View File

@@ -2,7 +2,10 @@ use futures::future::join_all;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use crate::{ use crate::{
config::Config, image::Image, registry::{check_auth, get_token}, utils::new_reqwest_client config::Config,
image::Image,
registry::{check_auth, get_token},
utils::new_reqwest_client,
}; };
use crate::registry::get_latest_digest; use crate::registry::get_latest_digest;
@@ -78,7 +81,5 @@ pub async fn get_updates(images: &[Image], config: &Config) -> Vec<Image> {
handles.push(future); handles.push(future);
} }
// Await all the futures // Await all the futures
let final_images = join_all(handles).await; join_all(handles).await
final_images
} }

View File

@@ -28,7 +28,7 @@ impl Config {
authentication: FxHashMap::default(), authentication: FxHashMap::default(),
theme: Theme::Default, theme: Theme::Default,
insecure_registries: Vec::with_capacity(0), insecure_registries: Vec::with_capacity(0),
socket: None socket: None,
} }
} }
/// Reads the config from the file path provided and returns the parsed result. /// Reads the config from the file path provided and returns the parsed result.

View File

@@ -2,7 +2,7 @@ use bollard::{models::ImageInspect, ClientVersion, Docker};
use futures::future::join_all; use futures::future::join_all;
use crate::{error, image::Image, config::Config}; use crate::{config::Config, error, image::Image};
fn create_docker_client(socket: Option<String>) -> Docker { fn create_docker_client(socket: Option<String>) -> Docker {
let client: Result<Docker, bollard::errors::Error> = match socket { let client: Result<Docker, bollard::errors::Error> = match socket {

View File

@@ -2,7 +2,10 @@ use std::time::Duration;
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use crate::{image::{Image, Status}, utils::{sort_image_vec, to_simple_json}}; use crate::{
image::{Image, Status},
utils::{sort_image_vec, to_simple_json},
};
pub fn print_updates(updates: &[Image], icons: &bool) { pub fn print_updates(updates: &[Image], icons: &bool) {
let sorted_images = sort_image_vec(updates); let sorted_images = sort_image_vec(updates);

View File

@@ -1,4 +1,7 @@
use std::fmt::Display;
use bollard::models::{ImageInspect, ImageSummary}; use bollard::models::{ImageInspect, ImageSummary};
use json::{object, JsonValue};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
@@ -6,7 +9,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, PartialEq)] #[derive(Clone, Debug, PartialEq, Default)]
pub struct Image { pub struct Image {
pub reference: String, pub reference: String,
pub registry: Option<String>, pub registry: Option<String>,
@@ -14,7 +17,8 @@ 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> pub error: Option<String>,
pub time_ms: i64
} }
impl Image { impl Image {
@@ -108,24 +112,38 @@ impl Image {
pub fn has_update(&self) -> Status { pub fn has_update(&self) -> Status {
if self.error.is_some() { if self.error.is_some() {
Status::Unknown(self.error.clone().unwrap()) Status::Unknown(self.error.clone().unwrap())
} else if self.local_digests.as_ref().unwrap().contains(&self.remote_digest.as_ref().unwrap()) { } else if self
.local_digests
.as_ref()
.unwrap()
.contains(self.remote_digest.as_ref().unwrap())
{
Status::UpToDate Status::UpToDate
} else { } else {
Status::UpdateAvailable Status::UpdateAvailable
} }
} }
}
impl Default for Image { /// Converts image data into a `JsonValue`
fn default() -> Self { pub fn to_json(&self) -> JsonValue {
Self { let has_update = self.has_update();
reference: String::new(), object! {
registry: None, reference: self.reference.clone(),
repository: None, parts: object! {
tag: None, registry: self.registry.clone(),
local_digests: None, repository: self.repository.clone(),
remote_digest: None, tag: self.tag.clone()
error: None },
local_digests: self.local_digests.clone(),
remote_digest: self.remote_digest.clone(),
result: object! { // API here will have to change for semver
has_update: has_update.to_option_bool(),
error: match has_update {
Status::Unknown(e) => Some(e),
_ => None
}
},
time: self.time_ms
} }
} }
} }
@@ -142,16 +160,16 @@ static RE: Lazy<Regex> = Lazy::new(|| {
pub enum Status { pub enum Status {
UpToDate, UpToDate,
UpdateAvailable, UpdateAvailable,
Unknown(String) Unknown(String),
} }
impl ToString for Status { impl Display for Status {
fn to_string(&self) -> String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self { f.write_str(match &self {
Self::UpToDate => "Up to date", Self::UpToDate => "Up to date",
Self::UpdateAvailable => "Update available", Self::UpdateAvailable => "Update available",
Self::Unknown(_) => "Unknown" Self::Unknown(_) => "Unknown",
}.to_string() })
} }
} }
@@ -161,7 +179,7 @@ impl Status {
match &self { match &self {
Self::UpdateAvailable => Some(true), Self::UpdateAvailable => Some(true),
Self::UpToDate => Some(false), Self::UpToDate => Some(false),
Self::Unknown(_) => None Self::Unknown(_) => None,
} }
} }
} }

View File

@@ -1,5 +1,4 @@
use check::get_updates; use check::get_updates;
use chrono::Local;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use config::Config; use config::Config;
use docker::get_images_from_docker_daemon; use docker::get_images_from_docker_daemon;
@@ -7,6 +6,7 @@ use docker::get_images_from_docker_daemon;
use formatting::{print_raw_updates, print_updates, Spinner}; use formatting::{print_raw_updates, print_updates, Spinner};
#[cfg(feature = "server")] #[cfg(feature = "server")]
use server::serve; use server::serve;
use utils::timestamp;
use std::path::PathBuf; use std::path::PathBuf;
pub mod check; pub mod check;
@@ -67,9 +67,8 @@ async fn main() {
path => Some(PathBuf::from(path)), path => Some(PathBuf::from(path)),
}; };
let mut config = Config::new().load(cfg_path); let mut config = Config::new().load(cfg_path);
match cli.socket { if let Some(socket) = cli.socket {
Some(socket) => config.socket = Some(socket), config.socket = Some(socket)
None => ()
} }
match &cli.command { match &cli.command {
#[cfg(feature = "cli")] #[cfg(feature = "cli")]
@@ -78,7 +77,7 @@ async fn main() {
icons, icons,
raw, raw,
}) => { }) => {
let start = Local::now().timestamp_millis(); let start = timestamp();
let images = get_images_from_docker_daemon(&config, references).await; let images = get_images_from_docker_daemon(&config, references).await;
match raw { match raw {
true => { true => {
@@ -89,7 +88,7 @@ async fn main() {
let spinner = Spinner::new(); let spinner = Spinner::new();
let updates = get_updates(&images, &config).await; let updates = get_updates(&images, &config).await;
spinner.succeed(); spinner.succeed();
let end = Local::now().timestamp_millis(); let end = timestamp();
print_updates(&updates, icons); print_updates(&updates, icons);
info!("✨ Checked {} images in {}ms", updates.len(), end - start); info!("✨ Checked {} images in {}ms", updates.len(), end - start);
} }

View File

@@ -3,7 +3,7 @@ use json::JsonValue;
use http_auth::parse_challenges; use http_auth::parse_challenges;
use reqwest_middleware::ClientWithMiddleware; use reqwest_middleware::ClientWithMiddleware;
use crate::{config::Config, error, image::Image, warn}; use crate::{config::Config, error, image::Image, utils::timestamp, warn};
pub async fn check_auth( pub async fn check_auth(
registry: &str, registry: &str,
@@ -58,7 +58,10 @@ pub async fn get_latest_digest(
config: &Config, config: &Config,
client: &ClientWithMiddleware, client: &ClientWithMiddleware,
) -> Image { ) -> Image {
let protocol = if config.insecure_registries.contains(&image.registry.clone().unwrap()) let start = timestamp();
let protocol = if config
.insecure_registries
.contains(&image.registry.clone().unwrap())
{ {
"http" "http"
} else { } else {
@@ -83,14 +86,14 @@ pub async fn get_latest_digest(
if status == 401 { if status == 401 {
if token.is_some() { if token.is_some() {
warn!("Failed to authenticate to registry {} with token provided!\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() } return Image { remote_digest: None, error: Some(format!("Authentication token \"{}\" was not accepted", token.unwrap())), time_ms: timestamp() - start, ..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, error: Some("Registry requires authentication".to_string()), time_ms: timestamp() - start, ..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, error: Some("Image not found".to_string()), ..image.clone() } return Image { remote_digest: None, error: Some("Image not found".to_string()), time_ms: timestamp() - start, ..image.clone() }
} else { } else {
response response
} }
@@ -98,7 +101,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, error: Some("Connection to registry failed".to_string()), ..image.clone() } return Image { remote_digest: None, error: Some("Connection to registry failed".to_string()), time_ms: timestamp() - start, ..image.clone() }
} else { } else {
error!("Unexpected error: {}", e.to_string()) error!("Unexpected error: {}", e.to_string())
} }
@@ -107,6 +110,7 @@ pub async fn get_latest_digest(
match raw_response.headers().get("docker-content-digest") { match raw_response.headers().get("docker-content-digest") {
Some(digest) => Image { Some(digest) => Image {
remote_digest: Some(digest.to_str().unwrap().to_string()), remote_digest: Some(digest.to_str().unwrap().to_string()),
time_ms: timestamp() - start,
..image.clone() ..image.clone()
}, },
None => error!( None => error!(

View File

@@ -14,7 +14,12 @@ use xitca_web::{
}; };
use crate::{ use crate::{
check::get_updates, config::{Config, Theme}, docker::get_images_from_docker_daemon, image::Image, info, utils::{sort_image_vec, to_simple_json} check::get_updates,
config::{Config, Theme},
docker::get_images_from_docker_daemon,
image::Image,
info,
utils::{sort_image_vec, timestamp, to_full_json, to_simple_json},
}; };
const HTML: &str = include_str!("static/index.html"); const HTML: &str = include_str!("static/index.html");
@@ -31,7 +36,8 @@ pub async fn serve(port: &u16, config: &Config) -> std::io::Result<()> {
App::new() App::new()
.with_state(Arc::new(Mutex::new(data))) .with_state(Arc::new(Mutex::new(data)))
.at("/", get(handler_service(_static))) .at("/", get(handler_service(_static)))
.at("/json", get(handler_service(json))) .at("/api/v1/simple", get(handler_service(api_simple)))
.at("/api/v1/full", get(handler_service(api_full)))
.at("/refresh", get(handler_service(refresh))) .at("/refresh", get(handler_service(refresh)))
.at("/*", get(handler_service(_static))) .at("/*", get(handler_service(_static)))
.enclosed(Logger::new()) .enclosed(Logger::new())
@@ -77,9 +83,15 @@ async fn _static(data: StateRef<'_, Arc<Mutex<ServerData>>>, path: PathRef<'_>)
} }
} }
async fn json(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse { async fn api_simple(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
WebResponse::new(ResponseBody::from(json::stringify( WebResponse::new(ResponseBody::from(json::stringify(
data.lock().await.json.clone(), data.lock().await.simple_json.clone(),
)))
}
async fn api_full(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
WebResponse::new(ResponseBody::from(json::stringify(
data.lock().await.full_json.clone(),
))) )))
} }
@@ -91,7 +103,8 @@ async fn refresh(data: StateRef<'_, Arc<Mutex<ServerData>>>) -> WebResponse {
struct ServerData { struct ServerData {
template: String, template: String,
raw_updates: Vec<Image>, raw_updates: Vec<Image>,
json: JsonValue, simple_json: JsonValue,
full_json: JsonValue,
config: Config, config: Config,
theme: &'static str, theme: &'static str,
} }
@@ -101,10 +114,8 @@ impl ServerData {
let mut s = Self { let mut s = Self {
config: config.clone(), config: config.clone(),
template: String::new(), template: String::new(),
json: json::object! { simple_json: JsonValue::Null,
metrics: json::object! {}, full_json: JsonValue::Null,
images: json::object! {},
},
raw_updates: Vec::new(), raw_updates: Vec::new(),
theme: "neutral", theme: "neutral",
}; };
@@ -112,13 +123,13 @@ impl ServerData {
s s
} }
async fn refresh(&mut self) { async fn refresh(&mut self) {
let start = Local::now().timestamp_millis(); let start = timestamp();
if !self.raw_updates.is_empty() { if !self.raw_updates.is_empty() {
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_image_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 = timestamp();
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;
let template = liquid::ParserBuilder::with_stdlib() let template = liquid::ParserBuilder::with_stdlib()
@@ -131,18 +142,23 @@ impl ServerData {
.iter() .iter()
.map(|image| object!({"name": image.reference, "has_update": image.has_update().to_option_bool().to_value()}),) .map(|image| object!({"name": image.reference, "has_update": image.has_update().to_option_bool().to_value()}),)
.collect::<Vec<Object>>(); .collect::<Vec<Object>>();
self.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);
let last_updated = Local::now(); let last_updated = Local::now();
self.json["last_updated"] = last_updated self.simple_json["last_updated"] = last_updated
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
.to_string()
.into();
self.full_json["last_updated"] = last_updated
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true) .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
.to_string() .to_string()
.into(); .into();
self.theme = match &self.config.theme { self.theme = match &self.config.theme {
Theme::Default => "neutral", Theme::Default => "neutral",
Theme::Blue => "gray" Theme::Blue => "gray",
}; };
let globals = object!({ let globals = object!({
"metrics": [{"name": "Monitored images", "value": self.json["metrics"]["monitored_images"].as_usize()}, {"name": "Up to date", "value": self.json["metrics"]["up_to_date"].as_usize()}, {"name": "Updates available", "value": self.json["metrics"]["update_available"].as_usize()}, {"name": "Unknown", "value": self.json["metrics"]["unknown"].as_usize()}], "metrics": [{"name": "Monitored images", "value": self.simple_json["metrics"]["monitored_images"].as_usize()}, {"name": "Up to date", "value": self.simple_json["metrics"]["up_to_date"].as_usize()}, {"name": "Updates available", "value": self.simple_json["metrics"]["update_available"].as_usize()}, {"name": "Unknown", "value": self.simple_json["metrics"]["unknown"].as_usize()}],
"images": images, "images": images,
"last_updated": last_updated.format("%Y-%m-%d %H:%M:%S").to_string(), "last_updated": last_updated.format("%Y-%m-%d %H:%M:%S").to_string(),
"theme": &self.theme "theme": &self.theme

View File

@@ -1,3 +1,4 @@
use chrono::Local;
use json::{object, JsonValue}; 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};
@@ -8,29 +9,23 @@ use crate::image::{Image, Status};
pub fn sort_image_vec(updates: &[Image]) -> Vec<Image> { 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.has_update(), b.has_update()) { sorted_updates.sort_unstable_by(|a, b| match (a.has_update(), b.has_update()) {
(Status::UpdateAvailable, Status::UpdateAvailable) => { (Status::UpdateAvailable, Status::UpdateAvailable) => a.reference.cmp(&b.reference),
a.reference.cmp(&b.reference) (Status::UpdateAvailable, Status::UpToDate | Status::Unknown(_)) => {
std::cmp::Ordering::Less
} }
(Status::UpdateAvailable, Status::UpToDate | Status::Unknown(_)) => std::cmp::Ordering::Less,
(Status::UpToDate, Status::UpdateAvailable) => std::cmp::Ordering::Greater, (Status::UpToDate, Status::UpdateAvailable) => std::cmp::Ordering::Greater,
(Status::UpToDate, Status::UpToDate) => { (Status::UpToDate, Status::UpToDate) => a.reference.cmp(&b.reference),
a.reference.cmp(&b.reference)
},
(Status::UpToDate, Status::Unknown(_)) => std::cmp::Ordering::Less, (Status::UpToDate, Status::Unknown(_)) => std::cmp::Ordering::Less,
(Status::Unknown(_), Status::UpdateAvailable | Status::UpToDate) => std::cmp::Ordering::Greater, (Status::Unknown(_), Status::UpdateAvailable | Status::UpToDate) => {
(Status::Unknown(_), Status::Unknown(_)) => { std::cmp::Ordering::Greater
a.reference.cmp(&b.reference)
} }
(Status::Unknown(_), Status::Unknown(_)) => a.reference.cmp(&b.reference),
}); });
sorted_updates.to_vec() sorted_updates.to_vec()
} }
/// Takes a slice of `Image` objects and returns a `JsonValue` of update info. The output doesn't contain much detail /// Helper function to get metrics used in JSON output
pub fn to_simple_json(updates: &[Image]) -> JsonValue { pub fn get_metrics(updates: &[Image]) -> JsonValue {
let mut json_data: JsonValue = object! {
metrics: object! {},
images: object! {}
};
let mut up_to_date = 0; let mut up_to_date = 0;
let mut update_available = 0; let mut update_available = 0;
let mut unknown = 0; let mut unknown = 0;
@@ -39,23 +34,43 @@ pub fn to_simple_json(updates: &[Image]) -> JsonValue {
match has_update { match has_update {
Status::UpdateAvailable => { Status::UpdateAvailable => {
update_available += 1; update_available += 1;
}, }
Status::UpToDate => { Status::UpToDate => {
up_to_date += 1; up_to_date += 1;
}, }
Status::Unknown(_) => { Status::Unknown(_) => {
unknown += 1; unknown += 1;
} }
}; };
let _ = json_data["images"].insert(&image.reference, has_update.to_option_bool());
}); });
let _ = json_data["metrics"].insert("monitored_images", updates.len()); object! {
let _ = json_data["metrics"].insert("up_to_date", up_to_date); monitored_images: updates.len(),
let _ = json_data["metrics"].insert("update_available", update_available); up_to_date: up_to_date,
let _ = json_data["metrics"].insert("unknown", unknown); update_available: update_available,
unknown: unknown
}
}
/// 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! {
metrics: get_metrics(updates),
images: object! {}
};
updates.iter().for_each(|image| {
let _ = json_data["images"].insert(&image.reference, image.has_update().to_option_bool());
});
json_data json_data
} }
/// Takes a slice of `Image` objects and returns a `JsonValue` of update info. All image data is included, useful for debugging.
pub fn to_full_json(updates: &[Image]) -> JsonValue {
object! {
metrics: get_metrics(updates),
images: updates.iter().map(|image| image.to_json()).collect::<Vec<JsonValue>>(),
}
}
// Logging // Logging
/// This macro is an alternative to panic. It prints the message you give it and exits the process with code 1, without printing a stack trace. Useful for when the program has to exit due to a user error or something unexpected which is unrelated to the program (e.g. a failed web request) /// This macro is an alternative to panic. It prints the message you give it and exits the process with code 1, without printing a stack trace. Useful for when the program has to exit due to a user error or something unexpected which is unrelated to the program (e.g. a failed web request)
@@ -89,6 +104,7 @@ macro_rules! debug {
}) })
} }
/// Creates a new reqwest client with automatic retries
pub fn new_reqwest_client() -> ClientWithMiddleware { pub fn new_reqwest_client() -> ClientWithMiddleware {
ClientBuilder::new(reqwest::Client::new()) ClientBuilder::new(reqwest::Client::new())
.with(RetryTransientMiddleware::new_with_policy( .with(RetryTransientMiddleware::new_with_policy(
@@ -97,6 +113,10 @@ pub fn new_reqwest_client() -> ClientWithMiddleware {
.build() .build()
} }
pub fn timestamp() -> i64 {
Local::now().timestamp_millis()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -107,25 +127,39 @@ mod tests {
// Create test objects // Create test objects
let update_available_1 = Image { let update_available_1 = Image {
reference: "busybox".to_string(), reference: "busybox".to_string(),
local_digests: Some(vec!["some_digest".to_string(), "some_other_digest".to_string()]), local_digests: Some(vec![
"some_digest".to_string(),
"some_other_digest".to_string(),
]),
remote_digest: Some("latest_digest".to_string()), remote_digest: Some("latest_digest".to_string()),
..Default::default() ..Default::default()
}; };
let update_available_2 = Image { let update_available_2 = Image {
reference: "library/alpine".to_string(), 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 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()), remote_digest: Some("latest_digest".to_string()),
..Default::default() ..Default::default()
}; };
let up_to_date_1 = Image { let up_to_date_1 = Image {
reference: "docker:dind".to_string(), reference: "docker:dind".to_string(),
local_digests: Some(vec!["some_digest".to_string(), "some_other_digest".to_string(), "latest_digest".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()), remote_digest: Some("latest_digest".to_string()),
..Default::default() ..Default::default()
}; };
let up_to_date_2 = Image { let up_to_date_2 = Image {
reference: "ghcr.io/sergi0g/cup".to_string(), reference: "ghcr.io/sergi0g/cup".to_string(),
local_digests: Some(vec!["some_digest".to_string(), "some_other_digest".to_string(), "latest_digest".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()), remote_digest: Some("latest_digest".to_string()),
..Default::default() ..Default::default()
}; };
@@ -139,8 +173,22 @@ mod tests {
error: Some("whoops".to_string()), error: Some("whoops".to_string()),
..Default::default() ..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 input_vec = vec![
let expected_vec = vec![update_available_1, update_available_2, up_to_date_1, up_to_date_2, unknown_1, unknown_2]; 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 // Sort the vec
let sorted_vec = sort_image_vec(&input_vec); let sorted_vec = sort_image_vec(&input_vec);
@@ -148,4 +196,4 @@ mod tests {
// Check results // Check results
assert_eq!(sorted_vec, expected_vec); assert_eq!(sorted_vec, expected_vec);
} }
} }