mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-18 09:53:43 -05:00
Nearly complete versioning support. Fixed old bugs where not all tags were fetched.
This commit is contained in:
68
src/utils/json.rs
Normal file
68
src/utils/json.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
// Functions that return JSON data, used for generating output and API responses
|
||||
|
||||
use json::{object, JsonValue};
|
||||
|
||||
use crate::structs::{image::Image, status::Status};
|
||||
|
||||
/// Helper function to get metrics used in JSON output
|
||||
pub fn get_metrics(updates: &[Image]) -> JsonValue {
|
||||
let mut up_to_date = 0;
|
||||
let mut major_updates = 0;
|
||||
let mut minor_updates = 0;
|
||||
let mut patch_updates = 0;
|
||||
let mut other_updates = 0;
|
||||
let mut unknown = 0;
|
||||
updates.iter().for_each(|image| {
|
||||
let has_update = image.has_update();
|
||||
match has_update {
|
||||
Status::UpdateMajor => {
|
||||
major_updates += 1;
|
||||
}
|
||||
Status::UpdateMinor => {
|
||||
minor_updates += 1;
|
||||
}
|
||||
Status::UpdatePatch => {
|
||||
patch_updates += 1;
|
||||
}
|
||||
Status::UpdateAvailable => {
|
||||
other_updates += 1;
|
||||
}
|
||||
Status::UpToDate => {
|
||||
up_to_date += 1;
|
||||
}
|
||||
Status::Unknown(_) => {
|
||||
unknown += 1;
|
||||
}
|
||||
};
|
||||
});
|
||||
object! {
|
||||
monitored_images: updates.len(),
|
||||
up_to_date: up_to_date,
|
||||
updates_available: major_updates + minor_updates + patch_updates + other_updates,
|
||||
major_updates: major_updates,
|
||||
minor_updates: minor_updates,
|
||||
patch_updates: patch_updates,
|
||||
other_updates: other_updates,
|
||||
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>>(),
|
||||
}
|
||||
}
|
||||
13
src/utils/link.rs
Normal file
13
src/utils/link.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use http_link::parse_link_header;
|
||||
use reqwest::Url;
|
||||
|
||||
use crate::error;
|
||||
|
||||
pub fn parse_link(link: &str, base: &str) -> String {
|
||||
match parse_link_header(link, &Url::from_str(base).unwrap()) {
|
||||
Ok(l) => l[0].target.to_string(),
|
||||
Err(e) => error!("Failed to parse link! {}", e)
|
||||
}
|
||||
}
|
||||
32
src/utils/logging.rs
Normal file
32
src/utils/logging.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
// Logging utilites
|
||||
|
||||
/// 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)
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($($arg:tt)*) => ({
|
||||
eprintln!("\x1b[38:5:204mERROR \x1b[0m {}", format!($($arg)*));
|
||||
std::process::exit(1);
|
||||
})
|
||||
}
|
||||
|
||||
// A small macro to print in yellow as a warning
|
||||
#[macro_export]
|
||||
macro_rules! warn {
|
||||
($($arg:tt)*) => ({
|
||||
eprintln!("\x1b[38:5:192mWARN \x1b[0m {}", format!($($arg)*));
|
||||
})
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
($($arg:tt)*) => ({
|
||||
println!("\x1b[38:5:86mINFO \x1b[0m {}", format!($($arg)*));
|
||||
})
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! debug {
|
||||
($($arg:tt)*) => ({
|
||||
println!("\x1b[38:5:63mDEBUG \x1b[0m {}", format!($($arg)*));
|
||||
})
|
||||
}
|
||||
8
src/utils/misc.rs
Normal file
8
src/utils/misc.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
// Miscellaneous utility functions that are too small to go in a separate file
|
||||
|
||||
use chrono::Local;
|
||||
|
||||
/// Gets the current timestamp. Mainly exists so I don't have to type this one line of code ;-)
|
||||
pub fn timestamp() -> i64 {
|
||||
Local::now().timestamp_millis()
|
||||
}
|
||||
7
src/utils/mod.rs
Normal file
7
src/utils/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod json;
|
||||
pub mod logging;
|
||||
pub mod misc;
|
||||
pub mod reference;
|
||||
pub mod request;
|
||||
pub mod sort_update_vec;
|
||||
pub mod link;
|
||||
46
src/utils/reference.rs
Normal file
46
src/utils/reference.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::error;
|
||||
|
||||
const DEFAULT_REGISTRY: &str = "registry-1.docker.io";
|
||||
|
||||
/// Takes an image and splits it into registry, repository and tag, based on the reference.
|
||||
/// For example, `ghcr.io/sergi0g/cup:latest` becomes `['ghcr.io', 'sergi0g/cup', 'latest']`.
|
||||
pub fn split(reference: &str) -> (String, String, String) {
|
||||
match REFERENCE_REGEX.captures(reference) {
|
||||
Some(c) => {
|
||||
let registry = match c.name("registry") {
|
||||
Some(registry) => registry.as_str().to_owned(),
|
||||
None => String::from(DEFAULT_REGISTRY),
|
||||
};
|
||||
return (
|
||||
registry.clone(),
|
||||
match c.name("repository") {
|
||||
Some(repository) => {
|
||||
let repo = repository.as_str().to_owned();
|
||||
if !repo.contains('/') && registry == DEFAULT_REGISTRY {
|
||||
format!("library/{}", repo)
|
||||
} else {
|
||||
repo
|
||||
}
|
||||
}
|
||||
None => error!("Failed to parse image {}", reference),
|
||||
},
|
||||
match c.name("tag") {
|
||||
Some(tag) => tag.as_str().to_owned(),
|
||||
None => String::from("latest"),
|
||||
},
|
||||
);
|
||||
}
|
||||
None => error!("Failed to parse image {}", reference),
|
||||
}
|
||||
}
|
||||
|
||||
/// Regex to match Docker image references against, so registry, repository and tag can be extracted.
|
||||
static REFERENCE_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r#"^(?P<name>(?:(?P<registry>(?:(?:localhost|[\w-]+(?:\.[\w-]+)+)(?::\d+)?)|[\w]+:\d+)/)?(?P<repository>[a-z0-9_.-]+(?:/[a-z0-9_.-]+)*))(?::(?P<tag>[\w][\w.-]{0,127}))?$"#, // From https://regex101.com/r/nmSDPA/1
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
55
src/utils/request.rs
Normal file
55
src/utils/request.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use http_auth::parse_challenges;
|
||||
use json::JsonValue;
|
||||
use reqwest::Response;
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// Parses the www-authenticate header the registry sends into a challenge URL
|
||||
pub fn parse_www_authenticate(www_auth: &str) -> String {
|
||||
let challenges = parse_challenges(www_auth).unwrap();
|
||||
if !challenges.is_empty() {
|
||||
let challenge = &challenges[0];
|
||||
if challenge.scheme == "Bearer" {
|
||||
format!(
|
||||
"{}?service={}",
|
||||
challenge.params[0].1.as_escaped(),
|
||||
challenge.params[1].1.as_escaped()
|
||||
)
|
||||
} else {
|
||||
error!("Unsupported scheme {}", &challenge.scheme)
|
||||
}
|
||||
} else {
|
||||
error!("No challenge provided by the server");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_protocol(registry: &String, insecure_registries: &[String]) -> String {
|
||||
if insecure_registries.contains(registry) {
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn to_bearer_string(token: &Option<&String>) -> Option<String> {
|
||||
token.as_ref().map(|t| format!("Bearer {}", t))
|
||||
}
|
||||
|
||||
pub async fn get_response_body(response: Response) -> String {
|
||||
match response.text().await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
error!("Failed to parse registry response into string!\n{}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_json(body: &str) -> JsonValue {
|
||||
match json::parse(body) {
|
||||
Ok(parsed) => parsed,
|
||||
Err(e) => {
|
||||
error!("Failed to parse server response\n{}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/utils/sort_update_vec.rs
Normal file
94
src/utils/sort_update_vec.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use crate::structs::image::Image;
|
||||
|
||||
/// Sorts the update vector alphabetically and where Some(true) > Some(false) > None
|
||||
pub fn sort_image_vec(updates: &[Image]) -> Vec<Image> {
|
||||
let mut sorted_updates = updates.to_vec();
|
||||
sorted_updates.sort_unstable_by_key(|img| img.has_update());
|
||||
sorted_updates.to_vec()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::structs::image::DigestInfo;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Test the `sort_update_vec` function
|
||||
/// TODO: test semver as well
|
||||
#[test]
|
||||
fn test_ordering() {
|
||||
// Create test objects
|
||||
let update_available_1 = Image {
|
||||
reference: "busybox".to_string(),
|
||||
digest_info: Some(DigestInfo {
|
||||
local_digests: 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(),
|
||||
digest_info: Some(DigestInfo {
|
||||
local_digests: vec!["some_digest".to_string(), "some_other_digest".to_string()],
|
||||
remote_digest: Some("latest_digest".to_string()),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let up_to_date_1 = Image {
|
||||
reference: "docker:dind".to_string(),
|
||||
digest_info: Some(DigestInfo {
|
||||
local_digests: vec![
|
||||
"some_digest".to_string(),
|
||||
"some_other_digest".to_string(),
|
||||
"latest_digest".to_string(),
|
||||
],
|
||||
remote_digest: Some("latest_digest".to_string()),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let up_to_date_2 = Image {
|
||||
reference: "ghcr.io/sergi0g/cup".to_string(),
|
||||
digest_info: Some(DigestInfo {
|
||||
local_digests: vec![
|
||||
"some_digest".to_string(),
|
||||
"some_other_digest".to_string(),
|
||||
"latest_digest".to_string(),
|
||||
],
|
||||
remote_digest: Some("latest_digest".to_string()),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
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