mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-17 09:33:38 -05:00
OMG WE CAN DO SEMVER FOR THE CLI AND THE RESULTS LOOK CORRECT
This commit is contained in:
@@ -8,8 +8,6 @@ use crate::{
|
|||||||
utils::new_reqwest_client,
|
utils::new_reqwest_client,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::registry::get_latest_digest;
|
|
||||||
|
|
||||||
/// Trait for a type that implements a function `unique` that removes any duplicates.
|
/// Trait for a type that implements a function `unique` that removes any duplicates.
|
||||||
/// In this case, it will be used for a Vec.
|
/// In this case, it will be used for a Vec.
|
||||||
pub trait Unique<T> {
|
pub trait Unique<T> {
|
||||||
@@ -77,7 +75,7 @@ pub async fn get_updates(images: &[Image], config: &Config) -> Vec<Image> {
|
|||||||
// Loop through images and get the latest digest for each
|
// Loop through images and get the latest digest for each
|
||||||
for image in images {
|
for image in images {
|
||||||
let token = tokens.get(&image.registry.as_ref().unwrap()).unwrap();
|
let token = tokens.get(&image.registry.as_ref().unwrap()).unwrap();
|
||||||
let future = get_latest_digest(image, token.as_ref(), config, &client);
|
let future = image.check(token.as_ref(), config, &client);
|
||||||
handles.push(future);
|
handles.push(future);
|
||||||
}
|
}
|
||||||
// Await all the futures
|
// Await all the futures
|
||||||
|
|||||||
@@ -17,15 +17,17 @@ pub fn print_updates(updates: &[Image], icons: &bool) {
|
|||||||
let description = has_update.to_string();
|
let description = has_update.to_string();
|
||||||
let icon = if *icons {
|
let icon = if *icons {
|
||||||
match has_update {
|
match has_update {
|
||||||
Status::UpdateAvailable => "\u{f0aa} ",
|
|
||||||
Status::UpToDate => "\u{f058} ",
|
Status::UpToDate => "\u{f058} ",
|
||||||
Status::Unknown(_) => "\u{f059} ",
|
Status::Unknown(_) => "\u{f059} ",
|
||||||
|
_ => "\u{f0aa} ",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
let color = match has_update {
|
let color = match has_update {
|
||||||
Status::UpdateAvailable => "\u{001b}[38;5;12m",
|
Status::UpdateAvailable | Status::UpdatePatch => "\u{001b}[38;5;12m",
|
||||||
|
Status::UpdateMinor => "\u{001b}[38;5;3m",
|
||||||
|
Status::UpdateMajor => "\u{001b}[38;5;1m",
|
||||||
Status::UpToDate => "\u{001b}[38;5;2m",
|
Status::UpToDate => "\u{001b}[38;5;2m",
|
||||||
Status::Unknown(_) => "\u{001b}[38;5;8m",
|
Status::Unknown(_) => "\u{001b}[38;5;8m",
|
||||||
};
|
};
|
||||||
|
|||||||
148
src/image.rs
148
src/image.rs
@@ -1,11 +1,16 @@
|
|||||||
use std::fmt::Display;
|
use std::{cmp::Ordering, fmt::Display};
|
||||||
|
|
||||||
use bollard::models::{ImageInspect, ImageSummary};
|
use bollard::models::{ImageInspect, ImageSummary};
|
||||||
use json::{object, JsonValue};
|
use json::{object, JsonValue};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use reqwest_middleware::ClientWithMiddleware;
|
||||||
|
|
||||||
use crate::error;
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
error,
|
||||||
|
registry::{get_latest_digest, get_latest_tag},
|
||||||
|
};
|
||||||
|
|
||||||
/// 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
|
||||||
@@ -17,6 +22,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 semver_tag: Option<SemVer>,
|
||||||
|
pub latest_remote_tag: Option<SemVer>,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
pub time_ms: i64,
|
pub time_ms: i64,
|
||||||
}
|
}
|
||||||
@@ -41,6 +48,7 @@ impl Image {
|
|||||||
image.registry = Some(registry);
|
image.registry = Some(registry);
|
||||||
image.repository = Some(repository);
|
image.repository = Some(repository);
|
||||||
image.tag = Some(tag);
|
image.tag = Some(tag);
|
||||||
|
image.semver_tag = image.get_version();
|
||||||
|
|
||||||
return Some(image);
|
return Some(image);
|
||||||
}
|
}
|
||||||
@@ -70,6 +78,7 @@ impl Image {
|
|||||||
image.registry = Some(registry);
|
image.registry = Some(registry);
|
||||||
image.repository = Some(repository);
|
image.repository = Some(repository);
|
||||||
image.tag = Some(tag);
|
image.tag = Some(tag);
|
||||||
|
image.semver_tag = image.get_version();
|
||||||
|
|
||||||
return Some(image);
|
return Some(image);
|
||||||
}
|
}
|
||||||
@@ -112,6 +121,11 @@ 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.latest_remote_tag.is_some() {
|
||||||
|
self.latest_remote_tag
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.to_status(self.semver_tag.as_ref().unwrap())
|
||||||
} else if self
|
} else if self
|
||||||
.local_digests
|
.local_digests
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -151,6 +165,19 @@ impl Image {
|
|||||||
pub fn get_version(&self) -> Option<SemVer> {
|
pub fn get_version(&self) -> Option<SemVer> {
|
||||||
get_version(self.tag.as_ref().unwrap())
|
get_version(self.tag.as_ref().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if the image has an update
|
||||||
|
pub async fn check(
|
||||||
|
&self,
|
||||||
|
token: Option<&String>,
|
||||||
|
config: &Config,
|
||||||
|
client: &ClientWithMiddleware,
|
||||||
|
) -> Self {
|
||||||
|
match &self.semver_tag {
|
||||||
|
Some(version) => get_latest_tag(self, version, token, config, client).await,
|
||||||
|
None => get_latest_digest(self, token, config, client).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to parse the tag into semver parts. Should have been included in impl Image, but that would make the tests more complicated
|
/// Tries to parse the tag into semver parts. Should have been included in impl Image, but that would make the tests more complicated
|
||||||
@@ -179,14 +206,8 @@ pub fn get_version(tag: &str) -> Option<SemVer> {
|
|||||||
Some(major) => major.as_str().parse().unwrap(),
|
Some(major) => major.as_str().parse().unwrap(),
|
||||||
None => return None,
|
None => return None,
|
||||||
};
|
};
|
||||||
let minor: i32 = match c.name("minor") {
|
let minor: Option<i32> = c.name("minor").map(|minor| minor.as_str().parse().unwrap());
|
||||||
Some(minor) => minor.as_str().parse().unwrap(),
|
let patch: Option<i32> = c.name("patch").map(|patch| patch.as_str().parse().unwrap());
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
let patch: i32 = match c.name("patch") {
|
|
||||||
Some(patch) => patch.as_str().parse().unwrap(),
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
Some(SemVer {
|
Some(SemVer {
|
||||||
major,
|
major,
|
||||||
minor,
|
minor,
|
||||||
@@ -212,9 +233,13 @@ static SEMVER: Lazy<Regex> = Lazy::new(|| {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/// Enum for image status
|
/// Enum for image status
|
||||||
|
#[derive(Ord, Eq, PartialEq, PartialOrd)]
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
UpToDate,
|
UpdateMajor,
|
||||||
|
UpdateMinor,
|
||||||
|
UpdatePatch,
|
||||||
UpdateAvailable,
|
UpdateAvailable,
|
||||||
|
UpToDate,
|
||||||
Unknown(String),
|
Unknown(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +248,9 @@ impl Display for Status {
|
|||||||
f.write_str(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::UpdateMajor => "Major update",
|
||||||
|
Self::UpdateMinor => "Minor update",
|
||||||
|
Self::UpdatePatch => "Patch update",
|
||||||
Self::Unknown(_) => "Unknown",
|
Self::Unknown(_) => "Unknown",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -232,18 +260,74 @@ impl Status {
|
|||||||
// Converts the Status into an Option<bool> (useful for JSON serialization)
|
// Converts the Status into an Option<bool> (useful for JSON serialization)
|
||||||
pub fn to_option_bool(&self) -> Option<bool> {
|
pub fn to_option_bool(&self) -> Option<bool> {
|
||||||
match &self {
|
match &self {
|
||||||
Self::UpdateAvailable => Some(true),
|
|
||||||
Self::UpToDate => Some(false),
|
Self::UpToDate => Some(false),
|
||||||
Self::Unknown(_) => None,
|
Self::Unknown(_) => None,
|
||||||
|
_ => Some(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct SemVer {
|
pub struct SemVer {
|
||||||
major: i32,
|
pub major: i32,
|
||||||
minor: i32,
|
pub minor: Option<i32>,
|
||||||
patch: i32,
|
pub patch: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SemVer {
|
||||||
|
fn to_status(&self, base: &Self) -> Status {
|
||||||
|
if self.major == base.major {
|
||||||
|
match (self.minor, base.minor) {
|
||||||
|
(Some(a_minor), Some(b_minor)) => {
|
||||||
|
if a_minor == b_minor {
|
||||||
|
match (self.patch, base.patch) {
|
||||||
|
(Some(a_patch), Some(b_patch)) => {
|
||||||
|
if a_patch == b_patch {
|
||||||
|
unreachable!()
|
||||||
|
} else {
|
||||||
|
Status::UpdatePatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Status::UpdateMinor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Status::UpdateMajor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for SemVer {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
let major_ordering = self.major.cmp(&other.major);
|
||||||
|
match major_ordering {
|
||||||
|
Ordering::Equal => match (self.minor, other.minor) {
|
||||||
|
(Some(self_minor), Some(other_minor)) => {
|
||||||
|
let minor_ordering = self_minor.cmp(&other_minor);
|
||||||
|
match minor_ordering {
|
||||||
|
Ordering::Equal => match (self.patch, other.patch) {
|
||||||
|
(Some(self_patch), Some(other_patch)) => self_patch.cmp(&other_patch),
|
||||||
|
_ => Ordering::Equal,
|
||||||
|
},
|
||||||
|
_ => minor_ordering,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ordering::Equal,
|
||||||
|
},
|
||||||
|
_ => major_ordering,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for SemVer {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -253,21 +337,21 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
fn semver() {
|
fn semver() {
|
||||||
assert_eq!(get_version("5.3.2" ).unwrap(), SemVer { major: 5, minor: 3, patch: 2 });
|
assert_eq!(get_version("5.3.2" ), Some(SemVer { major: 5, minor: Some(3), patch: Some(2) }));
|
||||||
assert_eq!(get_version("14" ).unwrap(), SemVer { major: 14, minor: 0, patch: 0 });
|
assert_eq!(get_version("14" ), Some(SemVer { major: 14, minor: Some(0), patch: Some(0) }));
|
||||||
assert_eq!(get_version("v0.107.53" ).unwrap(), SemVer { major: 0, minor: 107, patch: 53 });
|
assert_eq!(get_version("v0.107.53" ), Some(SemVer { major: 0, minor: Some(107), patch: Some(53) }));
|
||||||
assert_eq!(get_version("12-alpine" ).unwrap(), SemVer { major: 12, minor: 0, patch: 0 });
|
assert_eq!(get_version("12-alpine" ), Some(SemVer { major: 12, minor: Some(0), patch: Some(0) }));
|
||||||
assert_eq!(get_version("0.9.5-nginx" ).unwrap(), SemVer { major: 0, minor: 9, patch: 5 });
|
assert_eq!(get_version("0.9.5-nginx" ), Some(SemVer { major: 0, minor: Some(9), patch: Some(5) }));
|
||||||
assert_eq!(get_version("v27.0" ).unwrap(), SemVer { major: 27, minor: 0, patch: 0 });
|
assert_eq!(get_version("v27.0" ), Some(SemVer { major: 27, minor: Some(0), patch: Some(0) }));
|
||||||
assert_eq!(get_version("16.1" ).unwrap(), SemVer { major: 16, minor: 1, patch: 0 });
|
assert_eq!(get_version("16.1" ), Some(SemVer { major: 16, minor: Some(1), patch: Some(0) }));
|
||||||
assert_eq!(get_version("version-1.5.6" ).unwrap(), SemVer { major: 1, minor: 5, patch: 6 });
|
assert_eq!(get_version("version-1.5.6" ), Some(SemVer { major: 1, minor: Some(5), patch: Some(6) }));
|
||||||
assert_eq!(get_version("15.4-alpine" ).unwrap(), SemVer { major: 15, minor: 4, patch: 0 });
|
assert_eq!(get_version("15.4-alpine" ), Some(SemVer { major: 15, minor: Some(4), patch: Some(0) }));
|
||||||
assert_eq!(get_version("pg14-v0.2.0" ).unwrap(), SemVer { major: 0, minor: 2, patch: 0 });
|
assert_eq!(get_version("pg14-v0.2.0" ), Some(SemVer { major: 0, minor: Some(2), patch: Some(0) }));
|
||||||
assert_eq!(get_version("18-jammy-full.s6-v0.88.0").unwrap(), SemVer { major: 0, minor: 88, patch: 0 });
|
assert_eq!(get_version("18-jammy-full.s6-v0.88.0"), Some(SemVer { major: 0, minor: Some(88), patch: Some(0) }));
|
||||||
assert_eq!(get_version("fpm-2.1.0-prod" ).unwrap(), SemVer { major: 2, minor: 1, patch: 0 });
|
assert_eq!(get_version("fpm-2.1.0-prod" ), Some(SemVer { major: 2, minor: Some(1), patch: Some(0) }));
|
||||||
assert_eq!(get_version("7.3.3.50" ).unwrap(), SemVer { major: 7, minor: 3, patch: 3 });
|
assert_eq!(get_version("7.3.3.50" ), Some(SemVer { major: 7, minor: Some(3), patch: Some(3) }));
|
||||||
assert_eq!(get_version("1.21.11-0" ).unwrap(), SemVer { major: 1, minor: 21, patch: 11 });
|
assert_eq!(get_version("1.21.11-0" ), Some(SemVer { major: 1, minor: Some(21), patch: Some(11) }));
|
||||||
assert_eq!(get_version("4.1.2.1-full" ).unwrap(), SemVer { major: 4, minor: 1, patch: 2 });
|
assert_eq!(get_version("4.1.2.1-full" ), Some(SemVer { major: 4, minor: Some(1), patch: Some(2) }));
|
||||||
assert_eq!(get_version("v4.0.3-ls215" ).unwrap(), SemVer { major: 4, minor: 0, patch: 3 });
|
assert_eq!(get_version("v4.0.3-ls215" ), Some(SemVer { major: 4, minor: Some(0), patch: Some(3) }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
149
src/registry.rs
149
src/registry.rs
@@ -3,7 +3,13 @@ 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, utils::timestamp, warn};
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
error,
|
||||||
|
image::{get_version, Image, SemVer},
|
||||||
|
utils::timestamp,
|
||||||
|
warn,
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn check_auth(
|
pub async fn check_auth(
|
||||||
registry: &str,
|
registry: &str,
|
||||||
@@ -85,23 +91,23 @@ pub async fn get_latest_digest(
|
|||||||
let status = response.status();
|
let status = response.status();
|
||||||
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())), time_ms: timestamp() - start, ..image.clone() }
|
return Image { 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", image.registry.as_ref().unwrap());
|
||||||
return Image { remote_digest: None, error: Some("Registry requires authentication".to_string()), time_ms: timestamp() - start, ..image.clone() }
|
return Image { 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()), time_ms: timestamp() - start, ..image.clone() }
|
return Image { error: Some("Image not found".to_string()), time_ms: timestamp() - start, ..image.clone() }
|
||||||
} else {
|
} else {
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.is_connect() {
|
if e.is_connect() {
|
||||||
warn!("Connection to registry failed.");
|
warn!("Connection to registry {} failed.", image.registry.as_ref().unwrap());
|
||||||
return Image { remote_digest: None, error: Some("Connection to registry failed".to_string()), time_ms: timestamp() - start, ..image.clone() }
|
return Image { 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())
|
||||||
}
|
}
|
||||||
@@ -134,9 +140,7 @@ pub async fn get_token(
|
|||||||
image.repository.as_ref().unwrap()
|
image.repository.as_ref().unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let mut base_request = client
|
let mut base_request = client.get(&final_url);
|
||||||
.get(&final_url)
|
|
||||||
.header("Accept", "application/vnd.oci.image.index.v1+json"); // Seems to be unnecessary. Will probably remove in the future
|
|
||||||
base_request = match credentials {
|
base_request = match credentials {
|
||||||
Some(creds) => base_request.header("Authorization", &format!("Basic {}", creds)),
|
Some(creds) => base_request.header("Authorization", &format!("Basic {}", creds)),
|
||||||
None => base_request,
|
None => base_request,
|
||||||
@@ -165,6 +169,129 @@ pub async fn get_token(
|
|||||||
parsed_token_response["token"].to_string()
|
parsed_token_response["token"].to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_latest_tag(
|
||||||
|
image: &Image,
|
||||||
|
base: &SemVer,
|
||||||
|
token: Option<&String>,
|
||||||
|
config: &Config,
|
||||||
|
client: &ClientWithMiddleware,
|
||||||
|
) -> Image {
|
||||||
|
let start = timestamp();
|
||||||
|
|
||||||
|
// Start creating request
|
||||||
|
let protocol = if config
|
||||||
|
.insecure_registries
|
||||||
|
.contains(&image.registry.clone().unwrap())
|
||||||
|
{
|
||||||
|
"http"
|
||||||
|
} else {
|
||||||
|
"https"
|
||||||
|
};
|
||||||
|
let mut request = client.get(format!(
|
||||||
|
"{}://{}/v2/{}/tags/list",
|
||||||
|
protocol,
|
||||||
|
&image.registry.as_ref().unwrap(),
|
||||||
|
&image.repository.as_ref().unwrap(),
|
||||||
|
));
|
||||||
|
if let Some(t) = token {
|
||||||
|
request = request.header("Authorization", &format!("Bearer {}", t));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send request
|
||||||
|
let raw_response = match request.header("Accept", "application/json").send().await {
|
||||||
|
Ok(response) => {
|
||||||
|
let status = response.status();
|
||||||
|
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 {
|
||||||
|
error: Some(format!(
|
||||||
|
"Authentication token \"{}\" was not accepted",
|
||||||
|
token.unwrap()
|
||||||
|
)),
|
||||||
|
time_ms: timestamp() - start,
|
||||||
|
..image.clone()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Registry {} requires authentication",
|
||||||
|
image.registry.as_ref().unwrap()
|
||||||
|
);
|
||||||
|
return Image {
|
||||||
|
error: Some("Registry requires authentication".to_string()),
|
||||||
|
time_ms: timestamp() - start,
|
||||||
|
..image.clone()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if status == 404 {
|
||||||
|
warn!("Image {:?} not found", &image);
|
||||||
|
return Image {
|
||||||
|
error: Some("Image not found".to_string()),
|
||||||
|
time_ms: timestamp() - start,
|
||||||
|
..image.clone()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
match response.text().await {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to parse registry response into string!\n{}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if e.is_connect() {
|
||||||
|
warn!(
|
||||||
|
"Connection to registry {} failed.",
|
||||||
|
image.registry.as_ref().unwrap()
|
||||||
|
);
|
||||||
|
return Image {
|
||||||
|
error: Some("Connection to registry failed".to_string()),
|
||||||
|
time_ms: timestamp() - start,
|
||||||
|
..image.clone()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
error!("Unexpected error: {}", e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let parsed_response: JsonValue = match json::parse(&raw_response) {
|
||||||
|
Ok(parsed) => parsed,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to parse server response\n{}", e)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let tag = parsed_response["tags"]
|
||||||
|
.members()
|
||||||
|
.filter_map(|tag| get_version(&tag.to_string()))
|
||||||
|
.filter(|tag| match (base.minor, tag.minor) {
|
||||||
|
(Some(_), Some(_)) | (None, None) => {
|
||||||
|
matches!((base.patch, tag.patch), (Some(_), Some(_)) | (None, None))
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
.max();
|
||||||
|
match tag {
|
||||||
|
Some(t) => {
|
||||||
|
if t == *base {
|
||||||
|
// Tags are equal so we'll compare digests
|
||||||
|
get_latest_digest(image, token, config, client).await
|
||||||
|
} else {
|
||||||
|
Image {
|
||||||
|
latest_remote_tag: Some(t),
|
||||||
|
time_ms: timestamp() - start,
|
||||||
|
..image.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_www_authenticate(www_auth: &str) -> String {
|
fn parse_www_authenticate(www_auth: &str) -> String {
|
||||||
let challenges = parse_challenges(www_auth).unwrap();
|
let challenges = parse_challenges(www_auth).unwrap();
|
||||||
if !challenges.is_empty() {
|
if !challenges.is_empty() {
|
||||||
|
|||||||
35
src/utils.rs
35
src/utils.rs
@@ -8,32 +8,32 @@ use crate::image::{Image, Status};
|
|||||||
/// Sorts the update vector alphabetically and where Some(true) > Some(false) > None
|
/// Sorts the update vector alphabetically and where Some(true) > Some(false) > None
|
||||||
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_key(|img| img.has_update());
|
||||||
(Status::UpdateAvailable, Status::UpdateAvailable) => a.reference.cmp(&b.reference),
|
|
||||||
(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::Unknown(_)) => std::cmp::Ordering::Less,
|
|
||||||
(Status::Unknown(_), Status::UpdateAvailable | Status::UpToDate) => {
|
|
||||||
std::cmp::Ordering::Greater
|
|
||||||
}
|
|
||||||
(Status::Unknown(_), Status::Unknown(_)) => a.reference.cmp(&b.reference),
|
|
||||||
});
|
|
||||||
sorted_updates.to_vec()
|
sorted_updates.to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function to get metrics used in JSON output
|
/// Helper function to get metrics used in JSON output
|
||||||
pub fn get_metrics(updates: &[Image]) -> JsonValue {
|
pub fn get_metrics(updates: &[Image]) -> JsonValue {
|
||||||
let mut up_to_date = 0;
|
let mut up_to_date = 0;
|
||||||
let mut update_available = 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;
|
let mut unknown = 0;
|
||||||
updates.iter().for_each(|image| {
|
updates.iter().for_each(|image| {
|
||||||
let has_update = image.has_update();
|
let has_update = image.has_update();
|
||||||
match has_update {
|
match has_update {
|
||||||
|
Status::UpdateMajor => {
|
||||||
|
major_updates += 1;
|
||||||
|
}
|
||||||
|
Status::UpdateMinor => {
|
||||||
|
minor_updates += 1;
|
||||||
|
}
|
||||||
|
Status::UpdatePatch => {
|
||||||
|
patch_updates += 1;
|
||||||
|
}
|
||||||
Status::UpdateAvailable => {
|
Status::UpdateAvailable => {
|
||||||
update_available += 1;
|
other_updates += 1;
|
||||||
}
|
}
|
||||||
Status::UpToDate => {
|
Status::UpToDate => {
|
||||||
up_to_date += 1;
|
up_to_date += 1;
|
||||||
@@ -46,7 +46,10 @@ pub fn get_metrics(updates: &[Image]) -> JsonValue {
|
|||||||
object! {
|
object! {
|
||||||
monitored_images: updates.len(),
|
monitored_images: updates.len(),
|
||||||
up_to_date: up_to_date,
|
up_to_date: up_to_date,
|
||||||
update_available: update_available,
|
major_updates: major_updates,
|
||||||
|
minor_updates: minor_updates,
|
||||||
|
patch_updates: patch_updates,
|
||||||
|
other_updates: other_updates,
|
||||||
unknown: unknown
|
unknown: unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user