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 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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
62
src/image.rs
62
src/image.rs
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/main.rs
11
src/main.rs
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
106
src/utils.rs
106
src/utils.rs
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user