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:
@@ -2,7 +2,10 @@ use futures::future::join_all;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
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;
|
||||
@@ -78,7 +81,5 @@ pub async fn get_updates(images: &[Image], config: &Config) -> Vec<Image> {
|
||||
handles.push(future);
|
||||
}
|
||||
// Await all the futures
|
||||
let final_images = join_all(handles).await;
|
||||
|
||||
final_images
|
||||
join_all(handles).await
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Config {
|
||||
authentication: FxHashMap::default(),
|
||||
theme: Theme::Default,
|
||||
insecure_registries: Vec::with_capacity(0),
|
||||
socket: None
|
||||
socket: None,
|
||||
}
|
||||
}
|
||||
/// Reads the config from the file path provided and returns the parsed result.
|
||||
|
||||
@@ -2,7 +2,7 @@ use bollard::{models::ImageInspect, ClientVersion, Docker};
|
||||
|
||||
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 {
|
||||
let client: Result<Docker, bollard::errors::Error> = match socket {
|
||||
|
||||
@@ -2,7 +2,10 @@ use std::time::Duration;
|
||||
|
||||
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) {
|
||||
let sorted_images = sort_image_vec(updates);
|
||||
|
||||
60
src/image.rs
60
src/image.rs
@@ -1,4 +1,7 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use bollard::models::{ImageInspect, ImageSummary};
|
||||
use json::{object, JsonValue};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
@@ -6,7 +9,7 @@ use crate::error;
|
||||
|
||||
/// Image struct that contains all information that may be needed by a function.
|
||||
/// It's designed to be passed around between functions
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub struct Image {
|
||||
pub reference: String,
|
||||
pub registry: Option<String>,
|
||||
@@ -14,7 +17,8 @@ pub struct Image {
|
||||
pub tag: Option<String>,
|
||||
pub local_digests: Option<Vec<String>>,
|
||||
pub remote_digest: Option<String>,
|
||||
pub error: Option<String>
|
||||
pub error: Option<String>,
|
||||
pub time_ms: i64
|
||||
}
|
||||
|
||||
impl Image {
|
||||
@@ -108,24 +112,38 @@ impl Image {
|
||||
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()) {
|
||||
} 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
|
||||
/// Converts image data into a `JsonValue`
|
||||
pub fn to_json(&self) -> JsonValue {
|
||||
let has_update = self.has_update();
|
||||
object! {
|
||||
reference: self.reference.clone(),
|
||||
parts: object! {
|
||||
registry: self.registry.clone(),
|
||||
repository: self.repository.clone(),
|
||||
tag: self.tag.clone()
|
||||
},
|
||||
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 {
|
||||
UpToDate,
|
||||
UpdateAvailable,
|
||||
Unknown(String)
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl ToString for Status {
|
||||
fn to_string(&self) -> String {
|
||||
match &self {
|
||||
impl Display for Status {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match &self {
|
||||
Self::UpToDate => "Up to date",
|
||||
Self::UpdateAvailable => "Update available",
|
||||
Self::Unknown(_) => "Unknown"
|
||||
}.to_string()
|
||||
Self::Unknown(_) => "Unknown",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +179,7 @@ impl Status {
|
||||
match &self {
|
||||
Self::UpdateAvailable => Some(true),
|
||||
Self::UpToDate => Some(false),
|
||||
Self::Unknown(_) => None
|
||||
Self::Unknown(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/main.rs
11
src/main.rs
@@ -1,5 +1,4 @@
|
||||
use check::get_updates;
|
||||
use chrono::Local;
|
||||
use clap::{Parser, Subcommand};
|
||||
use config::Config;
|
||||
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};
|
||||
#[cfg(feature = "server")]
|
||||
use server::serve;
|
||||
use utils::timestamp;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub mod check;
|
||||
@@ -67,9 +67,8 @@ async fn main() {
|
||||
path => Some(PathBuf::from(path)),
|
||||
};
|
||||
let mut config = Config::new().load(cfg_path);
|
||||
match cli.socket {
|
||||
Some(socket) => config.socket = Some(socket),
|
||||
None => ()
|
||||
if let Some(socket) = cli.socket {
|
||||
config.socket = Some(socket)
|
||||
}
|
||||
match &cli.command {
|
||||
#[cfg(feature = "cli")]
|
||||
@@ -78,7 +77,7 @@ async fn main() {
|
||||
icons,
|
||||
raw,
|
||||
}) => {
|
||||
let start = Local::now().timestamp_millis();
|
||||
let start = timestamp();
|
||||
let images = get_images_from_docker_daemon(&config, references).await;
|
||||
match raw {
|
||||
true => {
|
||||
@@ -89,7 +88,7 @@ async fn main() {
|
||||
let spinner = Spinner::new();
|
||||
let updates = get_updates(&images, &config).await;
|
||||
spinner.succeed();
|
||||
let end = Local::now().timestamp_millis();
|
||||
let end = timestamp();
|
||||
print_updates(&updates, icons);
|
||||
info!("✨ Checked {} images in {}ms", updates.len(), end - start);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use json::JsonValue;
|
||||
use http_auth::parse_challenges;
|
||||
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(
|
||||
registry: &str,
|
||||
@@ -58,7 +58,10 @@ pub async fn get_latest_digest(
|
||||
config: &Config,
|
||||
client: &ClientWithMiddleware,
|
||||
) -> 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"
|
||||
} else {
|
||||
@@ -83,14 +86,14 @@ pub async fn get_latest_digest(
|
||||
if status == 401 {
|
||||
if token.is_some() {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
response
|
||||
}
|
||||
@@ -98,7 +101,7 @@ pub async fn get_latest_digest(
|
||||
Err(e) => {
|
||||
if e.is_connect() {
|
||||
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 {
|
||||
error!("Unexpected error: {}", e.to_string())
|
||||
}
|
||||
@@ -107,6 +110,7 @@ pub async fn get_latest_digest(
|
||||
match raw_response.headers().get("docker-content-digest") {
|
||||
Some(digest) => Image {
|
||||
remote_digest: Some(digest.to_str().unwrap().to_string()),
|
||||
time_ms: timestamp() - start,
|
||||
..image.clone()
|
||||
},
|
||||
None => error!(
|
||||
|
||||
@@ -14,7 +14,12 @@ use xitca_web::{
|
||||
};
|
||||
|
||||
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");
|
||||
@@ -31,7 +36,8 @@ pub async fn serve(port: &u16, config: &Config) -> std::io::Result<()> {
|
||||
App::new()
|
||||
.with_state(Arc::new(Mutex::new(data)))
|
||||
.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("/*", get(handler_service(_static)))
|
||||
.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(
|
||||
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 {
|
||||
template: String,
|
||||
raw_updates: Vec<Image>,
|
||||
json: JsonValue,
|
||||
simple_json: JsonValue,
|
||||
full_json: JsonValue,
|
||||
config: Config,
|
||||
theme: &'static str,
|
||||
}
|
||||
@@ -101,10 +114,8 @@ impl ServerData {
|
||||
let mut s = Self {
|
||||
config: config.clone(),
|
||||
template: String::new(),
|
||||
json: json::object! {
|
||||
metrics: json::object! {},
|
||||
images: json::object! {},
|
||||
},
|
||||
simple_json: JsonValue::Null,
|
||||
full_json: JsonValue::Null,
|
||||
raw_updates: Vec::new(),
|
||||
theme: "neutral",
|
||||
};
|
||||
@@ -112,13 +123,13 @@ impl ServerData {
|
||||
s
|
||||
}
|
||||
async fn refresh(&mut self) {
|
||||
let start = Local::now().timestamp_millis();
|
||||
let start = timestamp();
|
||||
if !self.raw_updates.is_empty() {
|
||||
info!("Refreshing data");
|
||||
}
|
||||
let images = get_images_from_docker_daemon(&self.config, &None).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);
|
||||
self.raw_updates = updates;
|
||||
let template = liquid::ParserBuilder::with_stdlib()
|
||||
@@ -131,18 +142,23 @@ impl ServerData {
|
||||
.iter()
|
||||
.map(|image| object!({"name": image.reference, "has_update": image.has_update().to_option_bool().to_value()}),)
|
||||
.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();
|
||||
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_string()
|
||||
.into();
|
||||
self.theme = match &self.config.theme {
|
||||
Theme::Default => "neutral",
|
||||
Theme::Blue => "gray"
|
||||
Theme::Blue => "gray",
|
||||
};
|
||||
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,
|
||||
"last_updated": last_updated.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
"theme": &self.theme
|
||||
|
||||
104
src/utils.rs
104
src/utils.rs
@@ -1,3 +1,4 @@
|
||||
use chrono::Local;
|
||||
use json::{object, JsonValue};
|
||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
|
||||
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
|
||||
@@ -8,29 +9,23 @@ use crate::image::{Image, Status};
|
||||
pub fn sort_image_vec(updates: &[Image]) -> Vec<Image> {
|
||||
let mut sorted_updates = updates.to_vec();
|
||||
sorted_updates.sort_unstable_by(|a, b| match (a.has_update(), b.has_update()) {
|
||||
(Status::UpdateAvailable, Status::UpdateAvailable) => {
|
||||
a.reference.cmp(&b.reference)
|
||||
(Status::UpdateAvailable, Status::UpdateAvailable) => 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::UpToDate) => {
|
||||
a.reference.cmp(&b.reference)
|
||||
},
|
||||
(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)
|
||||
(Status::Unknown(_), Status::UpdateAvailable | Status::UpToDate) => {
|
||||
std::cmp::Ordering::Greater
|
||||
}
|
||||
(Status::Unknown(_), Status::Unknown(_)) => a.reference.cmp(&b.reference),
|
||||
});
|
||||
sorted_updates.to_vec()
|
||||
}
|
||||
|
||||
/// 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: object! {},
|
||||
images: object! {}
|
||||
};
|
||||
/// Helper function to get metrics used in JSON output
|
||||
pub fn get_metrics(updates: &[Image]) -> JsonValue {
|
||||
let mut up_to_date = 0;
|
||||
let mut update_available = 0;
|
||||
let mut unknown = 0;
|
||||
@@ -39,23 +34,43 @@ pub fn to_simple_json(updates: &[Image]) -> JsonValue {
|
||||
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 _ = json_data["metrics"].insert("monitored_images", updates.len());
|
||||
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("unknown", unknown);
|
||||
object! {
|
||||
monitored_images: updates.len(),
|
||||
up_to_date: up_to_date,
|
||||
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
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
/// 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 {
|
||||
ClientBuilder::new(reqwest::Client::new())
|
||||
.with(RetryTransientMiddleware::new_with_policy(
|
||||
@@ -97,6 +113,10 @@ pub fn new_reqwest_client() -> ClientWithMiddleware {
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn timestamp() -> i64 {
|
||||
Local::now().timestamp_millis()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -107,25 +127,39 @@ mod tests {
|
||||
// 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()]),
|
||||
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
|
||||
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()]),
|
||||
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()]),
|
||||
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()
|
||||
};
|
||||
@@ -139,8 +173,22 @@ mod tests {
|
||||
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];
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user