mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-16 09:03:46 -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,
|
||||
};
|
||||
|
||||
use crate::registry::get_latest_digest;
|
||||
|
||||
/// Trait for a type that implements a function `unique` that removes any duplicates.
|
||||
/// In this case, it will be used for a Vec.
|
||||
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
|
||||
for image in images {
|
||||
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);
|
||||
}
|
||||
// Await all the futures
|
||||
|
||||
@@ -17,15 +17,17 @@ pub fn print_updates(updates: &[Image], icons: &bool) {
|
||||
let description = has_update.to_string();
|
||||
let icon = if *icons {
|
||||
match has_update {
|
||||
Status::UpdateAvailable => "\u{f0aa} ",
|
||||
Status::UpToDate => "\u{f058} ",
|
||||
Status::Unknown(_) => "\u{f059} ",
|
||||
_ => "\u{f0aa} ",
|
||||
}
|
||||
} else {
|
||||
""
|
||||
};
|
||||
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::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 json::{object, JsonValue};
|
||||
use once_cell::sync::Lazy;
|
||||
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.
|
||||
/// It's designed to be passed around between functions
|
||||
@@ -17,6 +22,8 @@ pub struct Image {
|
||||
pub tag: Option<String>,
|
||||
pub local_digests: Option<Vec<String>>,
|
||||
pub remote_digest: Option<String>,
|
||||
pub semver_tag: Option<SemVer>,
|
||||
pub latest_remote_tag: Option<SemVer>,
|
||||
pub error: Option<String>,
|
||||
pub time_ms: i64,
|
||||
}
|
||||
@@ -41,6 +48,7 @@ impl Image {
|
||||
image.registry = Some(registry);
|
||||
image.repository = Some(repository);
|
||||
image.tag = Some(tag);
|
||||
image.semver_tag = image.get_version();
|
||||
|
||||
return Some(image);
|
||||
}
|
||||
@@ -70,6 +78,7 @@ impl Image {
|
||||
image.registry = Some(registry);
|
||||
image.repository = Some(repository);
|
||||
image.tag = Some(tag);
|
||||
image.semver_tag = image.get_version();
|
||||
|
||||
return Some(image);
|
||||
}
|
||||
@@ -112,6 +121,11 @@ impl Image {
|
||||
pub fn has_update(&self) -> Status {
|
||||
if self.error.is_some() {
|
||||
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
|
||||
.local_digests
|
||||
.as_ref()
|
||||
@@ -151,6 +165,19 @@ impl Image {
|
||||
pub fn get_version(&self) -> Option<SemVer> {
|
||||
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
|
||||
@@ -179,14 +206,8 @@ pub fn get_version(tag: &str) -> Option<SemVer> {
|
||||
Some(major) => major.as_str().parse().unwrap(),
|
||||
None => return None,
|
||||
};
|
||||
let minor: i32 = match c.name("minor") {
|
||||
Some(minor) => minor.as_str().parse().unwrap(),
|
||||
None => 0,
|
||||
};
|
||||
let patch: i32 = match c.name("patch") {
|
||||
Some(patch) => patch.as_str().parse().unwrap(),
|
||||
None => 0,
|
||||
};
|
||||
let minor: Option<i32> = c.name("minor").map(|minor| minor.as_str().parse().unwrap());
|
||||
let patch: Option<i32> = c.name("patch").map(|patch| patch.as_str().parse().unwrap());
|
||||
Some(SemVer {
|
||||
major,
|
||||
minor,
|
||||
@@ -212,9 +233,13 @@ static SEMVER: Lazy<Regex> = Lazy::new(|| {
|
||||
});
|
||||
|
||||
/// Enum for image status
|
||||
#[derive(Ord, Eq, PartialEq, PartialOrd)]
|
||||
pub enum Status {
|
||||
UpToDate,
|
||||
UpdateMajor,
|
||||
UpdateMinor,
|
||||
UpdatePatch,
|
||||
UpdateAvailable,
|
||||
UpToDate,
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
@@ -223,6 +248,9 @@ impl Display for Status {
|
||||
f.write_str(match &self {
|
||||
Self::UpToDate => "Up to date",
|
||||
Self::UpdateAvailable => "Update available",
|
||||
Self::UpdateMajor => "Major update",
|
||||
Self::UpdateMinor => "Minor update",
|
||||
Self::UpdatePatch => "Patch update",
|
||||
Self::Unknown(_) => "Unknown",
|
||||
})
|
||||
}
|
||||
@@ -232,18 +260,74 @@ impl Status {
|
||||
// Converts the Status into an Option<bool> (useful for JSON serialization)
|
||||
pub fn to_option_bool(&self) -> Option<bool> {
|
||||
match &self {
|
||||
Self::UpdateAvailable => Some(true),
|
||||
Self::UpToDate => Some(false),
|
||||
Self::Unknown(_) => None,
|
||||
_ => Some(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct SemVer {
|
||||
major: i32,
|
||||
minor: i32,
|
||||
patch: i32,
|
||||
pub major: i32,
|
||||
pub minor: Option<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)]
|
||||
@@ -253,21 +337,21 @@ mod tests {
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn semver() {
|
||||
assert_eq!(get_version("5.3.2" ).unwrap(), SemVer { major: 5, minor: 3, patch: 2 });
|
||||
assert_eq!(get_version("14" ).unwrap(), SemVer { major: 14, minor: 0, patch: 0 });
|
||||
assert_eq!(get_version("v0.107.53" ).unwrap(), SemVer { major: 0, minor: 107, patch: 53 });
|
||||
assert_eq!(get_version("12-alpine" ).unwrap(), SemVer { major: 12, minor: 0, patch: 0 });
|
||||
assert_eq!(get_version("0.9.5-nginx" ).unwrap(), SemVer { major: 0, minor: 9, patch: 5 });
|
||||
assert_eq!(get_version("v27.0" ).unwrap(), SemVer { major: 27, minor: 0, patch: 0 });
|
||||
assert_eq!(get_version("16.1" ).unwrap(), SemVer { major: 16, minor: 1, patch: 0 });
|
||||
assert_eq!(get_version("version-1.5.6" ).unwrap(), SemVer { major: 1, minor: 5, patch: 6 });
|
||||
assert_eq!(get_version("15.4-alpine" ).unwrap(), SemVer { major: 15, minor: 4, patch: 0 });
|
||||
assert_eq!(get_version("pg14-v0.2.0" ).unwrap(), SemVer { major: 0, minor: 2, patch: 0 });
|
||||
assert_eq!(get_version("18-jammy-full.s6-v0.88.0").unwrap(), SemVer { major: 0, minor: 88, patch: 0 });
|
||||
assert_eq!(get_version("fpm-2.1.0-prod" ).unwrap(), SemVer { major: 2, minor: 1, patch: 0 });
|
||||
assert_eq!(get_version("7.3.3.50" ).unwrap(), SemVer { major: 7, minor: 3, patch: 3 });
|
||||
assert_eq!(get_version("1.21.11-0" ).unwrap(), SemVer { major: 1, minor: 21, patch: 11 });
|
||||
assert_eq!(get_version("4.1.2.1-full" ).unwrap(), SemVer { major: 4, minor: 1, patch: 2 });
|
||||
assert_eq!(get_version("v4.0.3-ls215" ).unwrap(), SemVer { major: 4, minor: 0, patch: 3 });
|
||||
assert_eq!(get_version("5.3.2" ), Some(SemVer { major: 5, minor: Some(3), patch: Some(2) }));
|
||||
assert_eq!(get_version("14" ), Some(SemVer { major: 14, minor: Some(0), patch: Some(0) }));
|
||||
assert_eq!(get_version("v0.107.53" ), Some(SemVer { major: 0, minor: Some(107), patch: Some(53) }));
|
||||
assert_eq!(get_version("12-alpine" ), Some(SemVer { major: 12, minor: Some(0), patch: Some(0) }));
|
||||
assert_eq!(get_version("0.9.5-nginx" ), Some(SemVer { major: 0, minor: Some(9), patch: Some(5) }));
|
||||
assert_eq!(get_version("v27.0" ), Some(SemVer { major: 27, minor: Some(0), patch: Some(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" ), Some(SemVer { major: 1, minor: Some(5), patch: Some(6) }));
|
||||
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" ), Some(SemVer { major: 0, minor: Some(2), patch: Some(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" ), Some(SemVer { major: 2, minor: Some(1), patch: Some(0) }));
|
||||
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" ), Some(SemVer { major: 1, minor: Some(21), patch: Some(11) }));
|
||||
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" ), 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 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(
|
||||
registry: &str,
|
||||
@@ -85,23 +91,23 @@ pub async fn get_latest_digest(
|
||||
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 { remote_digest: None, error: Some(format!("Authentication token \"{}\" was not accepted", token.unwrap())), time_ms: timestamp() - start, ..image.clone() }
|
||||
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");
|
||||
return Image { remote_digest: None, error: Some("Registry requires authentication".to_string()), time_ms: timestamp() - start, ..image.clone() }
|
||||
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 { 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 {
|
||||
response
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
if e.is_connect() {
|
||||
warn!("Connection to registry failed.");
|
||||
return Image { remote_digest: None, error: Some("Connection to registry failed".to_string()), time_ms: timestamp() - start, ..image.clone() }
|
||||
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())
|
||||
}
|
||||
@@ -134,9 +140,7 @@ pub async fn get_token(
|
||||
image.repository.as_ref().unwrap()
|
||||
);
|
||||
}
|
||||
let mut base_request = client
|
||||
.get(&final_url)
|
||||
.header("Accept", "application/vnd.oci.image.index.v1+json"); // Seems to be unnecessary. Will probably remove in the future
|
||||
let mut base_request = client.get(&final_url);
|
||||
base_request = match credentials {
|
||||
Some(creds) => base_request.header("Authorization", &format!("Basic {}", creds)),
|
||||
None => base_request,
|
||||
@@ -165,6 +169,129 @@ pub async fn get_token(
|
||||
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 {
|
||||
let challenges = parse_challenges(www_auth).unwrap();
|
||||
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
|
||||
pub fn sort_image_vec(updates: &[Image]) -> Vec<Image> {
|
||||
let mut sorted_updates = updates.to_vec();
|
||||
sorted_updates.sort_unstable_by(|a, b| match (a.has_update(), b.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.sort_unstable_by_key(|img| img.has_update());
|
||||
sorted_updates.to_vec()
|
||||
}
|
||||
|
||||
/// Helper function to get metrics used in JSON output
|
||||
pub fn get_metrics(updates: &[Image]) -> JsonValue {
|
||||
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;
|
||||
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 => {
|
||||
update_available += 1;
|
||||
other_updates += 1;
|
||||
}
|
||||
Status::UpToDate => {
|
||||
up_to_date += 1;
|
||||
@@ -46,7 +46,10 @@ pub fn get_metrics(updates: &[Image]) -> JsonValue {
|
||||
object! {
|
||||
monitored_images: updates.len(),
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user