m/cup
1
0
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:
Sergio
2024-11-15 13:21:30 +02:00
parent c11b5e6432
commit d94abecf35
30 changed files with 1288 additions and 962 deletions

68
src/utils/json.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}
}
}

View 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);
}
}