mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-18 01:43:41 -05:00
V3
Many many many changes, honestly just read the release notes
This commit is contained in:
75
src/utils/json.rs
Normal file
75
src/utils/json.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
// Functions that return JSON data, used for generating output and API responses
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::structs::{status::Status, update::Update};
|
||||
|
||||
/// Helper function to get metrics used in JSON output
|
||||
pub fn get_metrics(updates: &[Update]) -> Value {
|
||||
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.get_status();
|
||||
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;
|
||||
}
|
||||
};
|
||||
});
|
||||
json!({
|
||||
"monitored_images": updates.len(),
|
||||
"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,
|
||||
"up_to_date": up_to_date,
|
||||
"unknown": unknown
|
||||
})
|
||||
}
|
||||
|
||||
/// Takes a slice of `Image` objects and returns a `Value` with update info. The output doesn't contain much detail
|
||||
pub fn to_simple_json(updates: &[Update]) -> Value {
|
||||
let mut update_map = Map::new();
|
||||
updates.iter().for_each(|update| {
|
||||
let _ = update_map.insert(
|
||||
update.reference.clone(),
|
||||
match update.result.has_update {
|
||||
Some(has_update) => Value::Bool(has_update),
|
||||
None => Value::Null,
|
||||
},
|
||||
);
|
||||
});
|
||||
let json_data: Value = json!({
|
||||
"metrics": get_metrics(updates),
|
||||
"images": updates,
|
||||
});
|
||||
json_data
|
||||
}
|
||||
|
||||
/// Takes a slice of `Image` objects and returns a `Value` with update info. All image data is included, useful for debugging.
|
||||
pub fn to_full_json(updates: &[Update]) -> Value {
|
||||
json!({
|
||||
"metrics": get_metrics(updates),
|
||||
"images": updates.iter().map(|update| serde_json::to_value(update).unwrap()).collect::<Vec<Value>>(),
|
||||
})
|
||||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
6
src/utils/mod.rs
Normal file
6
src/utils/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod json;
|
||||
pub mod link;
|
||||
pub mod reference;
|
||||
pub mod request;
|
||||
pub mod sort_update_vec;
|
||||
pub mod time;
|
||||
66
src/utils/reference.rs
Normal file
66
src/utils/reference.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
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) {
|
||||
let splits = reference.split('/').collect::<Vec<&str>>();
|
||||
let (registry, repository_and_tag) = match splits.len() {
|
||||
0 => unreachable!(),
|
||||
1 => (DEFAULT_REGISTRY, reference.to_string()),
|
||||
_ => {
|
||||
// Check if we're looking at a domain
|
||||
if splits[0] == "localhost" || splits[0].contains('.') || splits[0].contains(':') {
|
||||
(splits[0], splits[1..].join("/"))
|
||||
} else {
|
||||
(DEFAULT_REGISTRY, reference.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
let splits = repository_and_tag.split(':').collect::<Vec<&str>>();
|
||||
let (repository, tag) = match splits.len() {
|
||||
1 | 2 => {
|
||||
let repository_components = splits[0].split('/').collect::<Vec<&str>>();
|
||||
let repository = match repository_components.len() {
|
||||
0 => unreachable!(),
|
||||
1 => {
|
||||
if registry == DEFAULT_REGISTRY {
|
||||
format!("library/{}", repository_components[0])
|
||||
} else {
|
||||
splits[0].to_string()
|
||||
}
|
||||
}
|
||||
_ => splits[0].to_string(),
|
||||
};
|
||||
let tag = match splits.len() {
|
||||
1 => "latest",
|
||||
2 => splits[1],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
(repository, tag)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
(registry.to_string(), repository, tag.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn reference() {
|
||||
assert_eq!(split("alpine" ), (String::from(DEFAULT_REGISTRY ), String::from("library/alpine" ), String::from("latest")));
|
||||
assert_eq!(split("alpine:latest" ), (String::from(DEFAULT_REGISTRY ), String::from("library/alpine" ), String::from("latest")));
|
||||
assert_eq!(split("library/alpine" ), (String::from(DEFAULT_REGISTRY ), String::from("library/alpine" ), String::from("latest")));
|
||||
assert_eq!(split("localhost/test" ), (String::from("localhost" ), String::from("test" ), String::from("latest")));
|
||||
assert_eq!(split("localhost:1234/test" ), (String::from("localhost:1234" ), String::from("test" ), String::from("latest")));
|
||||
assert_eq!(split("test:1234/idk" ), (String::from("test:1234" ), String::from("idk" ), String::from("latest")));
|
||||
assert_eq!(split("alpine:3.7" ), (String::from(DEFAULT_REGISTRY ), String::from("library/alpine" ), String::from("3.7" )));
|
||||
assert_eq!(split("docker.example.com/examplerepo/alpine:3.7" ), (String::from("docker.example.com" ), String::from("examplerepo/alpine" ), String::from("3.7" )));
|
||||
assert_eq!(split("docker.example.com/examplerepo/alpine/test2:3.7" ), (String::from("docker.example.com" ), String::from("examplerepo/alpine/test2" ), String::from("3.7" )));
|
||||
assert_eq!(split("docker.example.com/examplerepo/alpine/test2/test3:3.7"), (String::from("docker.example.com" ), String::from("examplerepo/alpine/test2/test3"), String::from("3.7" )));
|
||||
assert_eq!(split("docker.example.com:5000/examplerepo/alpine:latest" ), (String::from("docker.example.com:5000"), String::from("examplerepo/alpine" ), String::from("latest")));
|
||||
assert_eq!(split("portainer/portainer:latest" ), (String::from(DEFAULT_REGISTRY ), String::from("portainer/portainer" ), String::from("latest")));
|
||||
}
|
||||
}
|
||||
68
src/utils/request.rs
Normal file
68
src/utils/request.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use http_auth::parse_challenges;
|
||||
use reqwest::Response;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{config::RegistryConfig, 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" {
|
||||
challenge
|
||||
.params
|
||||
.iter()
|
||||
.fold(String::new(), |acc, (key, value)| {
|
||||
if *key == "realm" {
|
||||
acc.to_owned() + value.as_escaped() + "?"
|
||||
} else {
|
||||
format!("{}&{}={}", acc, key, value.as_escaped())
|
||||
}
|
||||
})
|
||||
} else {
|
||||
error!("Unsupported scheme {}", &challenge.scheme)
|
||||
}
|
||||
} else {
|
||||
error!("No challenge provided by the server");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_protocol(
|
||||
registry: &str,
|
||||
registry_config: &FxHashMap<String, RegistryConfig>,
|
||||
) -> &'static str {
|
||||
match registry_config.get(registry) {
|
||||
Some(config) => {
|
||||
if config.insecure {
|
||||
"http"
|
||||
} else {
|
||||
"https"
|
||||
}
|
||||
}
|
||||
None => "https",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_bearer_string(token: &Option<&str>) -> 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) -> Value {
|
||||
match serde_json::from_str(body) {
|
||||
Ok(parsed) => parsed,
|
||||
Err(e) => {
|
||||
error!("Failed to parse server response\n{}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
130
src/utils/sort_update_vec.rs
Normal file
130
src/utils/sort_update_vec.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::structs::update::Update;
|
||||
|
||||
/// Sorts the update vector alphabetically and by Status
|
||||
pub fn sort_update_vec(updates: &[Update]) -> Vec<Update> {
|
||||
let mut sorted_updates = updates.to_vec();
|
||||
sorted_updates.sort_by(|a, b| {
|
||||
let cmp = a.get_status().cmp(&b.get_status());
|
||||
if cmp == Ordering::Equal {
|
||||
a.reference.cmp(&b.reference)
|
||||
} else {
|
||||
cmp
|
||||
}
|
||||
});
|
||||
sorted_updates.to_vec()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::structs::{status::Status, update::UpdateResult};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Test the `sort_update_vec` function
|
||||
/// We test for sorting based on status (Major > Minor > Patch > Digest > Up to date > Unknown) and that references are sorted alphabetically.
|
||||
#[test]
|
||||
fn test_ordering() {
|
||||
// Create test objects
|
||||
let major_update_1 = create_major_update("redis:6.2"); // We're ignoring the tag we passed here, that is tested in version.rs
|
||||
let major_update_2 = create_major_update("traefik:v3.0");
|
||||
let minor_update_1 = create_minor_update("mysql:8.0");
|
||||
let minor_update_2 = create_minor_update("rust:1.80.1-alpine");
|
||||
let patch_update_1 = create_patch_update("node:20");
|
||||
let patch_update_2 = create_patch_update("valkey/valkey:7.2-alpine");
|
||||
let digest_update_1 = create_digest_update("busybox");
|
||||
let digest_update_2 = create_digest_update("library/alpine");
|
||||
let up_to_date_1 = create_up_to_date("docker:dind");
|
||||
let up_to_date_2 = create_up_to_date("ghcr.io/sergi0g/cup");
|
||||
let unknown_1 = create_unknown("fake_registry.com/fake/Update");
|
||||
let unknown_2 = create_unknown("private_registry.io/private/Update");
|
||||
let input_vec = vec![
|
||||
major_update_2.clone(),
|
||||
unknown_2.clone(),
|
||||
minor_update_2.clone(),
|
||||
patch_update_2.clone(),
|
||||
up_to_date_1.clone(),
|
||||
unknown_1.clone(),
|
||||
patch_update_1.clone(),
|
||||
digest_update_2.clone(),
|
||||
minor_update_1.clone(),
|
||||
major_update_1.clone(),
|
||||
digest_update_1.clone(),
|
||||
up_to_date_2.clone(),
|
||||
];
|
||||
let expected_vec = vec![
|
||||
major_update_1,
|
||||
major_update_2,
|
||||
minor_update_1,
|
||||
minor_update_2,
|
||||
patch_update_1,
|
||||
patch_update_2,
|
||||
digest_update_1,
|
||||
digest_update_2,
|
||||
up_to_date_1,
|
||||
up_to_date_2,
|
||||
unknown_1,
|
||||
unknown_2,
|
||||
];
|
||||
|
||||
// Sort the vec
|
||||
let sorted_vec = sort_update_vec(&input_vec);
|
||||
|
||||
// Check results
|
||||
assert_eq!(sorted_vec, expected_vec);
|
||||
}
|
||||
|
||||
fn create_unknown(reference: &str) -> Update {
|
||||
Update {
|
||||
reference: reference.to_string(),
|
||||
status: Status::Unknown("".to_string()),
|
||||
result: UpdateResult {
|
||||
has_update: None,
|
||||
info: Default::default(),
|
||||
error: Some("Error".to_string()),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_up_to_date(reference: &str) -> Update {
|
||||
Update {
|
||||
reference: reference.to_string(),
|
||||
status: Status::UpToDate,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_digest_update(reference: &str) -> Update {
|
||||
Update {
|
||||
reference: reference.to_string(),
|
||||
status: Status::UpdateAvailable,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_patch_update(reference: &str) -> Update {
|
||||
Update {
|
||||
reference: reference.to_string(),
|
||||
status: Status::UpdatePatch,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_minor_update(reference: &str) -> Update {
|
||||
Update {
|
||||
reference: reference.to_string(),
|
||||
status: Status::UpdateMinor,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_major_update(reference: &str) -> Update {
|
||||
Update {
|
||||
reference: reference.to_string(),
|
||||
status: Status::UpdateMajor,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/utils/time.rs
Normal file
11
src/utils/time.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
// When you're too bored to type some things, you get this...
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub fn elapsed(start: SystemTime) -> u32 {
|
||||
start.elapsed().unwrap().as_millis() as u32
|
||||
}
|
||||
|
||||
pub fn now() -> SystemTime {
|
||||
SystemTime::now()
|
||||
}
|
||||
Reference in New Issue
Block a user