m/cup
1
0
mirror of https://github.com/sergi0g/cup.git synced 2025-11-17 17:43:37 -05:00
Many many many changes, honestly just read the release notes
This commit is contained in:
Sergio
2025-02-28 20:43:49 +02:00
committed by GitHub
parent b12acba745
commit 0f9c5d1466
141 changed files with 4527 additions and 5848 deletions

221
src/structs/image.rs Normal file
View File

@@ -0,0 +1,221 @@
use crate::{
error,
http::Client,
registry::{get_latest_digest, get_latest_tag},
structs::{status::Status, version::Version},
utils::reference::split,
Context,
};
use super::{
inspectdata::InspectData,
parts::Parts,
update::{DigestUpdateInfo, Update, UpdateInfo, UpdateResult, VersionUpdateInfo},
};
#[derive(Clone, PartialEq)]
#[cfg_attr(test, derive(Debug))]
pub struct DigestInfo {
pub local_digests: Vec<String>,
pub remote_digest: Option<String>,
}
#[derive(Clone, PartialEq)]
#[cfg_attr(test, derive(Debug))]
pub struct VersionInfo {
pub current_tag: Version,
pub latest_remote_tag: Option<Version>,
pub format_str: String,
}
/// Image struct that contains all information that may be needed by a function working with an image.
/// It's designed to be passed around between functions
#[derive(Clone, PartialEq, Default)]
#[cfg_attr(test, derive(Debug))]
pub struct Image {
pub reference: String,
pub parts: Parts,
pub digest_info: Option<DigestInfo>,
pub version_info: Option<VersionInfo>,
pub error: Option<String>,
pub time_ms: u32,
}
impl Image {
/// Creates and populates the fields of an Image object based on the ImageSummary from the Docker daemon
pub fn from_inspect_data<T: InspectData>(image: T) -> Option<Self> {
let tags = image.tags().unwrap();
let digests = image.digests().unwrap();
if !tags.is_empty() && !digests.is_empty() {
let reference = tags[0].clone();
let (registry, repository, tag) = split(&reference);
let version_tag = Version::from_tag(&tag);
let local_digests = digests
.iter()
.map(|digest| digest.split('@').collect::<Vec<&str>>()[1].to_string())
.collect();
Some(Self {
reference,
parts: Parts {
registry,
repository,
tag,
},
digest_info: Some(DigestInfo {
local_digests,
remote_digest: None,
}),
version_info: version_tag.map(|(vtag, format_str)| VersionInfo {
current_tag: vtag,
format_str,
latest_remote_tag: None,
}),
..Default::default()
})
} else {
None
}
}
/// Creates and populates the fields of an Image object based on a reference. If the tag is not recognized as a version string, exits the program with an error.
pub fn from_reference(reference: &str) -> Self {
let (registry, repository, tag) = split(reference);
let version_tag = Version::from_tag(&tag);
match version_tag {
Some((version, format_str)) => Self {
reference: reference.to_string(),
parts: Parts {
registry,
repository,
tag,
},
version_info: Some(VersionInfo {
current_tag: version,
format_str,
latest_remote_tag: None,
}),
..Default::default()
},
None => error!(
"Image {} is not available locally and does not have a recognizable tag format!",
reference
),
}
}
pub fn has_update(&self) -> Status {
if self.error.is_some() {
Status::Unknown(self.error.clone().unwrap())
} else {
match &self.version_info {
Some(data) => data
.latest_remote_tag
.as_ref()
.unwrap()
.to_status(&data.current_tag),
None => match &self.digest_info {
Some(data) => {
if data
.local_digests
.contains(data.remote_digest.as_ref().unwrap())
{
Status::UpToDate
} else {
Status::UpdateAvailable
}
}
None => unreachable!(), // I hope?
},
}
}
}
/// Converts image data into an `Update`
pub fn to_update(&self) -> Update {
let has_update = self.has_update();
let update_type = match has_update {
Status::UpToDate => "none",
Status::UpdateMajor | Status::UpdateMinor | Status::UpdatePatch => "version",
_ => "digest",
};
Update {
reference: self.reference.clone(),
parts: self.parts.clone(),
result: UpdateResult {
has_update: has_update.to_option_bool(),
info: match has_update {
Status::Unknown(_) => UpdateInfo::None,
_ => match update_type {
"version" => {
let (new_tag, format_str) = match &self.version_info {
Some(data) => (
data.latest_remote_tag.clone().unwrap(),
data.format_str.clone(),
),
_ => unreachable!(),
};
UpdateInfo::Version(VersionUpdateInfo {
version_update_type: match has_update {
Status::UpdateMajor => "major",
Status::UpdateMinor => "minor",
Status::UpdatePatch => "patch",
_ => unreachable!(),
}
.to_string(),
new_tag: format_str
.replacen("{}", &new_tag.major.to_string(), 1)
.replacen("{}", &new_tag.minor.unwrap_or(0).to_string(), 1)
.replacen("{}", &new_tag.patch.unwrap_or(0).to_string(), 1),
// Throwing these in, because they're useful for the CLI output, however we won't (de)serialize them
current_version: self
.version_info
.as_ref()
.unwrap()
.current_tag
.to_string(),
new_version: self
.version_info
.as_ref()
.unwrap()
.latest_remote_tag
.as_ref()
.unwrap()
.to_string(),
})
}
"digest" => {
let (local_digests, remote_digest) = match &self.digest_info {
Some(data) => {
(data.local_digests.clone(), data.remote_digest.clone())
}
_ => unreachable!(),
};
UpdateInfo::Digest(DigestUpdateInfo {
local_digests,
remote_digest,
})
}
"none" => UpdateInfo::None,
_ => unreachable!(),
},
},
error: self.error.clone(),
},
time: self.time_ms,
server: None,
status: has_update,
}
}
/// Checks if the image has an update
pub async fn check(&self, token: Option<&str>, ctx: &Context, client: &Client) -> Self {
match &self.version_info {
Some(data) => get_latest_tag(self, &data.current_tag, token, ctx, client).await,
None => match self.digest_info {
Some(_) => get_latest_digest(self, token, ctx, client).await,
None => unreachable!(),
},
}
}
}

View File

@@ -0,0 +1,26 @@
use bollard::secret::{ImageInspect, ImageSummary};
pub trait InspectData {
fn tags(&self) -> Option<&Vec<String>>;
fn digests(&self) -> Option<&Vec<String>>;
}
impl InspectData for ImageInspect {
fn tags(&self) -> Option<&Vec<String>> {
self.repo_tags.as_ref()
}
fn digests(&self) -> Option<&Vec<String>> {
self.repo_digests.as_ref()
}
}
impl InspectData for ImageSummary {
fn tags(&self) -> Option<&Vec<String>> {
Some(&self.repo_tags)
}
fn digests(&self) -> Option<&Vec<String>> {
Some(&self.repo_digests)
}
}

6
src/structs/mod.rs Normal file
View File

@@ -0,0 +1,6 @@
pub mod image;
pub mod inspectdata;
pub mod parts;
pub mod status;
pub mod update;
pub mod version;

8
src/structs/parts.rs Normal file
View File

@@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct Parts {
pub registry: String,
pub repository: String,
pub tag: String,
}

42
src/structs/status.rs Normal file
View File

@@ -0,0 +1,42 @@
use std::fmt::Display;
/// Enum for image status
#[derive(Ord, Eq, PartialEq, PartialOrd, Clone, Debug)]
pub enum Status {
UpdateMajor,
UpdateMinor,
UpdatePatch,
UpdateAvailable,
UpToDate,
Unknown(String),
}
impl Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
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",
})
}
}
impl Status {
// Converts the Status into an Option<bool> (useful for JSON serialization)
pub fn to_option_bool(&self) -> Option<bool> {
match &self {
Self::UpToDate => Some(false),
Self::Unknown(_) => None,
_ => Some(true),
}
}
}
impl Default for Status {
fn default() -> Self {
Self::Unknown("".to_string())
}
}

105
src/structs/update.rs Normal file
View File

@@ -0,0 +1,105 @@
use serde::{ser::SerializeStruct, Deserialize, Serialize};
use super::{parts::Parts, status::Status};
#[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Default))]
pub struct Update {
pub reference: String,
pub parts: Parts,
pub result: UpdateResult,
pub time: u32,
pub server: Option<String>,
#[serde(skip_serializing, skip_deserializing)]
pub status: Status,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Default))]
pub struct UpdateResult {
pub has_update: Option<bool>,
pub info: UpdateInfo,
pub error: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Default))]
#[serde(untagged)]
pub enum UpdateInfo {
#[cfg_attr(test, default)]
None,
Version(VersionUpdateInfo),
Digest(DigestUpdateInfo),
}
#[derive(Deserialize, Clone, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct VersionUpdateInfo {
pub version_update_type: String,
pub new_tag: String,
pub current_version: String,
pub new_version: String,
}
#[derive(Deserialize, Clone, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct DigestUpdateInfo {
pub local_digests: Vec<String>,
pub remote_digest: Option<String>,
}
impl Serialize for VersionUpdateInfo {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("VersionUpdateInfo", 5)?;
let _ = state.serialize_field("type", "version");
let _ = state.serialize_field("version_update_type", &self.version_update_type);
let _ = state.serialize_field("new_tag", &self.new_tag);
let _ = state.serialize_field("current_version", &self.current_version);
let _ = state.serialize_field("new_version", &self.new_version);
state.end()
}
}
impl Serialize for DigestUpdateInfo {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("DigestUpdateInfo", 3)?;
let _ = state.serialize_field("type", "digest");
let _ = state.serialize_field("local_digests", &self.local_digests);
let _ = state.serialize_field("remote_digest", &self.remote_digest);
state.end()
}
}
impl Update {
pub fn get_status(&self) -> Status {
match &self.status {
Status::Unknown(s) => {
if s.is_empty() {
match self.result.has_update {
Some(true) => match &self.result.info {
UpdateInfo::Version(info) => match info.version_update_type.as_str() {
"major" => Status::UpdateMajor,
"minor" => Status::UpdateMinor,
"patch" => Status::UpdatePatch,
_ => unreachable!(),
},
UpdateInfo::Digest(_) => Status::UpdateAvailable,
_ => unreachable!(),
},
Some(false) => Status::UpToDate,
None => Status::Unknown(self.result.error.clone().unwrap()),
}
} else {
self.status.clone()
}
}
status => status.clone(),
}
}
}

182
src/structs/version.rs Normal file
View File

@@ -0,0 +1,182 @@
use std::{cmp::Ordering, fmt::Display};
use once_cell::sync::Lazy;
use regex::Regex;
use super::status::Status;
/// Semver-like version struct
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Version {
pub major: u32,
pub minor: Option<u32>,
pub patch: Option<u32>,
}
impl Version {
/// Tries to parse the tag into semver-like parts. Returns a Version object and a string usable in format! with {} in the positions matches were found
pub fn from_tag(tag: &str) -> Option<(Self, String)> {
/// Heavily modified version of the official semver regex based on common tagging schemes for container images. Sometimes it matches more than once, but we'll try to select the best match.
static VERSION_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?P<major>0|[1-9][0-9]*)(?:\.(?P<minor>0|[1-9][0-9]*))?(?:\.(?P<patch>0|[1-9][0-9]*))?",
)
.unwrap()
});
let captures = VERSION_REGEX.captures_iter(tag);
// And now... terrible best match selection for everyone!
let mut max_matches = 0;
let mut best_match = None;
for capture in captures {
let mut count = 0;
for idx in 1..capture.len() {
if capture.get(idx).is_some() {
count += 1
} else {
break;
}
}
if count > max_matches {
max_matches = count;
best_match = Some(capture);
}
}
match best_match {
Some(c) => {
let mut positions = Vec::new();
let major: u32 = match c.name("major") {
Some(major) => {
positions.push((major.start(), major.end()));
match major.as_str().parse() {
Ok(m) => m,
Err(_) => return None
}
}
None => return None,
};
let minor: Option<u32> = c.name("minor").map(|minor| {
positions.push((minor.start(), minor.end()));
minor.as_str().parse().unwrap_or_else(|_| panic!("Minor version invalid in tag {}", tag))
});
let patch: Option<u32> = c.name("patch").map(|patch| {
positions.push((patch.start(), patch.end()));
patch.as_str().parse().unwrap_or_else(|_| panic!("Patch version invalid in tag {}", tag))
});
let mut format_str = tag.to_string();
positions.reverse();
positions.iter().for_each(|(start, end)| {
format_str.replace_range(*start..*end, "{}");
});
Some((
Version {
major,
minor,
patch,
},
format_str,
))
}
None => None,
}
}
pub fn to_status(&self, base: &Self) -> Status {
match self.major.cmp(&base.major) {
Ordering::Greater => Status::UpdateMajor,
Ordering::Equal => match (self.minor, base.minor) {
(Some(a_minor), Some(b_minor)) => match a_minor.cmp(&b_minor) {
Ordering::Greater => Status::UpdateMinor,
Ordering::Equal => match (self.patch, base.patch) {
(Some(a_patch), Some(b_patch)) => match a_patch.cmp(&b_patch) {
Ordering::Greater => Status::UpdatePatch,
Ordering::Equal => Status::UpToDate,
Ordering::Less => {
Status::Unknown(format!("Tag {} does not exist", base))
}
},
(None, None) => Status::UpToDate,
_ => unreachable!(),
},
Ordering::Less => Status::Unknown(format!("Tag {} does not exist", base)),
},
(None, None) => Status::UpToDate,
_ => unreachable!(
"Version error: {} and {} should either both be Some or None",
self, base
),
},
Ordering::Less => Status::Unknown(format!("Tag {} does not exist", base)),
}
}
}
impl Ord for Version {
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 Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&format!(
"{}{}{}",
self.major,
match self.minor {
Some(minor) => format!(".{}", minor),
None => String::new(),
},
match self.patch {
Some(patch) => format!(".{}", patch),
None => String::new(),
}
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[rustfmt::skip]
fn version() {
assert_eq!(Version::from_tag("5.3.2" ), Some((Version { major: 5, minor: Some(3), patch: Some(2) }, String::from("{}.{}.{}" ))));
assert_eq!(Version::from_tag("14" ), Some((Version { major: 14, minor: None, patch: None }, String::from("{}" ))));
assert_eq!(Version::from_tag("v0.107.53" ), Some((Version { major: 0, minor: Some(107), patch: Some(53) }, String::from("v{}.{}.{}" ))));
assert_eq!(Version::from_tag("12-alpine" ), Some((Version { major: 12, minor: None, patch: None }, String::from("{}-alpine" ))));
assert_eq!(Version::from_tag("0.9.5-nginx" ), Some((Version { major: 0, minor: Some(9), patch: Some(5) }, String::from("{}.{}.{}-nginx" ))));
assert_eq!(Version::from_tag("v27.0" ), Some((Version { major: 27, minor: Some(0), patch: None }, String::from("v{}.{}" ))));
assert_eq!(Version::from_tag("16.1" ), Some((Version { major: 16, minor: Some(1), patch: None }, String::from("{}.{}" ))));
assert_eq!(Version::from_tag("version-1.5.6" ), Some((Version { major: 1, minor: Some(5), patch: Some(6) }, String::from("version-{}.{}.{}" ))));
assert_eq!(Version::from_tag("15.4-alpine" ), Some((Version { major: 15, minor: Some(4), patch: None }, String::from("{}.{}-alpine" ))));
assert_eq!(Version::from_tag("pg14-v0.2.0" ), Some((Version { major: 0, minor: Some(2), patch: Some(0) }, String::from("pg14-v{}.{}.{}" ))));
assert_eq!(Version::from_tag("18-jammy-full.s6-v0.88.0"), Some((Version { major: 0, minor: Some(88), patch: Some(0) }, String::from("18-jammy-full.s6-v{}.{}.{}"))));
assert_eq!(Version::from_tag("fpm-2.1.0-prod" ), Some((Version { major: 2, minor: Some(1), patch: Some(0) }, String::from("fpm-{}.{}.{}-prod" ))));
assert_eq!(Version::from_tag("7.3.3.50" ), Some((Version { major: 7, minor: Some(3), patch: Some(3) }, String::from("{}.{}.{}.50" ))));
assert_eq!(Version::from_tag("1.21.11-0" ), Some((Version { major: 1, minor: Some(21), patch: Some(11) }, String::from("{}.{}.{}-0" ))));
assert_eq!(Version::from_tag("4.1.2.1-full" ), Some((Version { major: 4, minor: Some(1), patch: Some(2) }, String::from("{}.{}.{}.1-full" ))));
assert_eq!(Version::from_tag("v4.0.3-ls215" ), Some((Version { major: 4, minor: Some(0), patch: Some(3) }, String::from("v{}.{}.{}-ls215" ))));
}
}