mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-17 01:23:39 -05:00
refactor config
This commit is contained in:
10
src/check.rs
10
src/check.rs
@@ -80,22 +80,24 @@ async fn get_remote_updates(ctx: &Context, client: &Client, refresh: bool) -> Ve
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a list of updates for all images passed in.
|
/// Returns a list of updates for all images passed in.
|
||||||
|
/// TODO: Completely rewrite this and make nothing is missed
|
||||||
pub async fn get_updates(
|
pub async fn get_updates(
|
||||||
references: &Option<Vec<String>>, // If a user requested _specific_ references to be checked, this will have a value
|
references: &Option<Vec<String>>, // If a user requested _specific_ references to be checked, this will have a value
|
||||||
refresh: bool,
|
refresh: bool,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
) -> Vec<Update> {
|
) -> Vec<Update> {
|
||||||
let client = Client::new(ctx);
|
let client = Client::new(ctx);
|
||||||
|
|
||||||
// Merge references argument with references from config
|
// Merge references argument with references from config
|
||||||
let all_references = match &references {
|
let all_references = match &references {
|
||||||
Some(refs) => {
|
Some(refs) => {
|
||||||
if !ctx.config.images.extra.is_empty() {
|
if !ctx.config.extra_images.is_empty() {
|
||||||
refs.clone().extend_from_slice(&ctx.config.images.extra);
|
refs.clone().extend_from_slice(&ctx.config.extra_images);
|
||||||
}
|
}
|
||||||
|
refs.clone().extend_from_slice(&ctx.config.images.iter().filter(|(_, cfg)| cfg.include).map(|(reference, _)| reference).cloned().collect::<Vec<String>>());
|
||||||
refs
|
refs
|
||||||
}
|
}
|
||||||
None => &ctx.config.images.extra,
|
None => &ctx.config.extra_images,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get local images
|
// Get local images
|
||||||
|
|||||||
@@ -51,11 +51,21 @@ pub struct RegistryConfig {
|
|||||||
pub ignore: bool,
|
pub ignore: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, Default, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum TagType {
|
||||||
|
#[default]
|
||||||
|
Standard,
|
||||||
|
Extended
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct ImageConfig {
|
pub struct ImageConfig {
|
||||||
pub extra: Vec<String>,
|
pub include: bool, // Takes precedence over extra_images and excluded_images
|
||||||
pub exclude: Vec<String>,
|
pub tag_type: TagType,
|
||||||
|
pub ignore: UpdateType
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
@@ -63,8 +73,9 @@ pub struct ImageConfig {
|
|||||||
pub struct Config {
|
pub struct Config {
|
||||||
version: u8,
|
version: u8,
|
||||||
pub agent: bool,
|
pub agent: bool,
|
||||||
pub ignore_update_type: UpdateType,
|
pub images: FxHashMap<String, ImageConfig>,
|
||||||
pub images: ImageConfig,
|
pub extra_images: Vec<String>, // These two are here for convenience, using `images` for this purpose should also work.
|
||||||
|
pub excluded_images: Vec<String>, // Takes precedence over extra_images
|
||||||
#[serde(deserialize_with = "empty_as_none")]
|
#[serde(deserialize_with = "empty_as_none")]
|
||||||
pub refresh_interval: Option<String>,
|
pub refresh_interval: Option<String>,
|
||||||
pub registries: FxHashMap<String, RegistryConfig>,
|
pub registries: FxHashMap<String, RegistryConfig>,
|
||||||
@@ -73,13 +84,15 @@ pub struct Config {
|
|||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add helper methods that abstact away complex logic (i.e. functions that return all excluded images, extra images, etc based on the precedence rules set)
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
version: 3,
|
version: 3,
|
||||||
agent: false,
|
agent: false,
|
||||||
ignore_update_type: UpdateType::default(),
|
images: FxHashMap::default(),
|
||||||
images: ImageConfig::default(),
|
extra_images: Vec::new(),
|
||||||
|
excluded_images: Vec::new(),
|
||||||
refresh_interval: None,
|
refresh_interval: None,
|
||||||
registries: FxHashMap::default(),
|
registries: FxHashMap::default(),
|
||||||
servers: FxHashMap::default(),
|
servers: FxHashMap::default(),
|
||||||
@@ -103,11 +116,11 @@ impl Config {
|
|||||||
match key.as_str() {
|
match key.as_str() {
|
||||||
"CUP_AGENT" => config.agent = cfg.agent,
|
"CUP_AGENT" => config.agent = cfg.agent,
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
"CUP_IGNORE_UPDATE_TYPE" => swap!(config.ignore_update_type, cfg.ignore_update_type),
|
|
||||||
#[rustfmt::skip]
|
|
||||||
"CUP_REFRESH_INTERVAL" => swap!(config.refresh_interval, cfg.refresh_interval),
|
"CUP_REFRESH_INTERVAL" => swap!(config.refresh_interval, cfg.refresh_interval),
|
||||||
"CUP_SOCKET" => swap!(config.socket, cfg.socket),
|
"CUP_SOCKET" => swap!(config.socket, cfg.socket),
|
||||||
"CUP_THEME" => swap!(config.theme, cfg.theme),
|
"CUP_THEME" => swap!(config.theme, cfg.theme),
|
||||||
|
"CUP_EXTRA_IMAGES" => swap!(config.extra_images, cfg.extra_images),
|
||||||
|
"CUP_EXCLUDED_IMAGES" => swap!(config.excluded_images, cfg.excluded_images),
|
||||||
// The syntax for these is slightly more complicated, not sure if they should be enabled or not. Let's stick to simple types for now.
|
// The syntax for these is slightly more complicated, not sure if they should be enabled or not. Let's stick to simple types for now.
|
||||||
// "CUP_IMAGES" => swap!(config.images, cfg.images),
|
// "CUP_IMAGES" => swap!(config.images, cfg.images),
|
||||||
// "CUP_REGISTRIES" => swap!(config.registries, cfg.registries),
|
// "CUP_REGISTRIES" => swap!(config.registries, cfg.registries),
|
||||||
@@ -141,8 +154,8 @@ impl Config {
|
|||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(e) => error!("Unexpected error occured while parsing config: {}", e),
|
Err(e) => error!("Unexpected error occured while parsing config: {}", e),
|
||||||
};
|
};
|
||||||
if config.version != 3 {
|
if config.version != 4 {
|
||||||
error!("You are trying to run Cup with an incompatible config file! Please migrate your config file to the version 3, or if you have already done so, add a `version` key with the value `3`.")
|
error!("You are trying to run Cup with an incompatible config file! Please migrate your config file to the version 4, or if you have already done so, add a `version` key with the value `4`.")
|
||||||
}
|
}
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|||||||
134
src/registry.rs
134
src/registry.rs
@@ -1,4 +1,4 @@
|
|||||||
use std::time::SystemTime;
|
use std::{cmp::Ordering, time::SystemTime};
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
@@ -6,10 +6,7 @@ use crate::{
|
|||||||
config::UpdateType,
|
config::UpdateType,
|
||||||
error,
|
error,
|
||||||
http::Client,
|
http::Client,
|
||||||
structs::{
|
structs::{image::Image, version::Version},
|
||||||
image::{DigestInfo, Image, VersionInfo},
|
|
||||||
version::Version,
|
|
||||||
},
|
|
||||||
utils::{
|
utils::{
|
||||||
link::parse_link,
|
link::parse_link,
|
||||||
request::{
|
request::{
|
||||||
@@ -44,11 +41,11 @@ pub async fn check_auth(registry: &str, ctx: &Context, client: &Client) -> Optio
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_latest_digest(
|
pub async fn get_latest_digest(
|
||||||
image: &Image,
|
image: &mut Image,
|
||||||
token: Option<&str>,
|
token: Option<&str>,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
client: &Client,
|
client: &Client,
|
||||||
) -> Image {
|
) -> () {
|
||||||
ctx.logger
|
ctx.logger
|
||||||
.debug(format!("Checking for digest update to {}", image.reference));
|
.debug(format!("Checking for digest update to {}", image.reference));
|
||||||
let start = SystemTime::now();
|
let start = SystemTime::now();
|
||||||
@@ -69,29 +66,17 @@ pub async fn get_latest_digest(
|
|||||||
match response {
|
match response {
|
||||||
Ok(res) => match res.headers().get("docker-content-digest") {
|
Ok(res) => match res.headers().get("docker-content-digest") {
|
||||||
Some(digest) => {
|
Some(digest) => {
|
||||||
let local_digests = match &image.digest_info {
|
image.update_info.remote_digest = Some(digest.to_str().unwrap().to_owned());
|
||||||
Some(data) => data.local_digests.clone(),
|
|
||||||
None => return image.clone(),
|
|
||||||
};
|
|
||||||
Image {
|
|
||||||
digest_info: Some(DigestInfo {
|
|
||||||
remote_digest: Some(digest.to_str().unwrap().to_string()),
|
|
||||||
local_digests,
|
|
||||||
}),
|
|
||||||
time_ms: image.time_ms + time,
|
|
||||||
..image.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None => error!(
|
None => error!(
|
||||||
"Server returned invalid response! No docker-content-digest!\n{:#?}",
|
"Server returned invalid response! No docker-content-digest!\n{:#?}",
|
||||||
res
|
res
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
Err(error) => Image {
|
Err(error) => {
|
||||||
error: Some(error),
|
image.error = Some(error);
|
||||||
time_ms: image.time_ms + time,
|
image.time_ms = image.time_ms + elapsed(start)
|
||||||
..image.clone()
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,12 +102,12 @@ pub async fn get_token(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_latest_tag(
|
pub async fn get_latest_tag(
|
||||||
image: &Image,
|
image: &mut Image,
|
||||||
base: &Version,
|
base: &Version,
|
||||||
token: Option<&str>,
|
token: Option<&str>,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
client: &Client,
|
client: &Client,
|
||||||
) -> Image {
|
) -> () {
|
||||||
ctx.logger
|
ctx.logger
|
||||||
.debug(format!("Checking for tag update to {}", image.reference));
|
.debug(format!("Checking for tag update to {}", image.reference));
|
||||||
let start = now();
|
let start = now();
|
||||||
@@ -150,7 +135,7 @@ pub async fn get_latest_tag(
|
|||||||
&next_url.unwrap(),
|
&next_url.unwrap(),
|
||||||
&headers,
|
&headers,
|
||||||
base,
|
base,
|
||||||
&image.version_info.as_ref().unwrap().format_str,
|
&image.reference,
|
||||||
ctx,
|
ctx,
|
||||||
client,
|
client,
|
||||||
)
|
)
|
||||||
@@ -158,17 +143,22 @@ pub async fn get_latest_tag(
|
|||||||
{
|
{
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(message) => {
|
Err(message) => {
|
||||||
return Image {
|
image.error = Some(message);
|
||||||
error: Some(message),
|
image.time_ms += elapsed(start);
|
||||||
time_ms: image.time_ms + elapsed(start),
|
return;
|
||||||
..image.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tags.extend_from_slice(&new_tags);
|
tags.extend_from_slice(&new_tags);
|
||||||
next_url = next;
|
next_url = next;
|
||||||
}
|
}
|
||||||
let tag = tags.iter().max();
|
let tag = tags.iter().reduce(|a, b| match a.partial_cmp(b) {
|
||||||
|
Some(ordering) => match ordering {
|
||||||
|
Ordering::Greater => a,
|
||||||
|
Ordering::Equal => b,
|
||||||
|
Ordering::Less => b,
|
||||||
|
},
|
||||||
|
None => unreachable!(),
|
||||||
|
});
|
||||||
ctx.logger.debug(format!(
|
ctx.logger.debug(format!(
|
||||||
"Checked for tag update to {} in {}ms",
|
"Checked for tag update to {} in {}ms",
|
||||||
image.reference,
|
image.reference,
|
||||||
@@ -176,32 +166,17 @@ pub async fn get_latest_tag(
|
|||||||
));
|
));
|
||||||
match tag {
|
match tag {
|
||||||
Some(t) => {
|
Some(t) => {
|
||||||
if t == base && image.digest_info.is_some() {
|
if t == base && !image.info.local_digests.is_empty() {
|
||||||
// Tags are equal so we'll compare digests
|
// Tags are equal so we'll compare digests
|
||||||
ctx.logger.debug(format!(
|
ctx.logger.debug(format!(
|
||||||
"Tags for {} are equal, comparing digests.",
|
"Tags for {} are equal, comparing digests.",
|
||||||
image.reference
|
image.reference
|
||||||
));
|
));
|
||||||
get_latest_digest(
|
image.time_ms += elapsed(start);
|
||||||
&Image {
|
get_latest_digest(image, token, ctx, client).await
|
||||||
version_info: None, // Overwrite previous version info, since it isn't useful anymore (equal tags means up to date and an image is truly up to date when its digests are up to date, and we'll be checking those anyway)
|
|
||||||
time_ms: image.time_ms + elapsed(start),
|
|
||||||
..image.clone()
|
|
||||||
},
|
|
||||||
token,
|
|
||||||
ctx,
|
|
||||||
client,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
} else {
|
} else {
|
||||||
Image {
|
image.update_info.latest_version = Some(t.clone());
|
||||||
version_info: Some(VersionInfo {
|
image.time_ms += elapsed(start);
|
||||||
latest_remote_tag: Some(t.clone()),
|
|
||||||
..image.version_info.as_ref().unwrap().clone()
|
|
||||||
}),
|
|
||||||
time_ms: image.time_ms + elapsed(start),
|
|
||||||
..image.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => error!(
|
None => error!(
|
||||||
@@ -215,12 +190,12 @@ pub async fn get_extra_tags(
|
|||||||
url: &str,
|
url: &str,
|
||||||
headers: &[(&str, Option<&str>)],
|
headers: &[(&str, Option<&str>)],
|
||||||
base: &Version,
|
base: &Version,
|
||||||
format_str: &str,
|
reference: &str,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
client: &Client,
|
client: &Client,
|
||||||
) -> Result<(Vec<Version>, Option<String>), String> {
|
) -> Result<(Vec<Version>, Option<String>), String> {
|
||||||
let response = client.get(url, headers, false).await;
|
let response = client.get(url, headers, false).await;
|
||||||
|
let base_type = base.r#type();
|
||||||
match response {
|
match response {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
let next_url = res
|
let next_url = res
|
||||||
@@ -232,25 +207,38 @@ pub async fn get_extra_tags(
|
|||||||
.as_array()
|
.as_array()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|tag| Version::from_tag(tag.as_str().unwrap()))
|
.map(|tag| Version::from(tag.as_str().unwrap(), base_type.as_ref()))
|
||||||
.filter(|(tag, format_string)| match (base.minor, tag.minor) {
|
.filter(|tag| tag.r#type() == base_type)
|
||||||
(Some(_), Some(_)) | (None, None) => {
|
.filter(|tag| tag.partial_cmp(base).is_some())
|
||||||
matches!((base.patch, tag.patch), (Some(_), Some(_)) | (None, None))
|
.filter_map(|tag| {
|
||||||
&& format_str == *format_string
|
match ctx
|
||||||
|
.config
|
||||||
|
.images
|
||||||
|
.iter()
|
||||||
|
.filter(|&(i, _)| reference.starts_with(i))
|
||||||
|
.sorted_by(|(a, _), (b, _)| a.len().cmp(&b.len()))
|
||||||
|
.next()
|
||||||
|
.map(|(_, cfg)| &cfg.ignore)
|
||||||
|
.unwrap_or(&UpdateType::None)
|
||||||
|
{
|
||||||
|
// TODO: Please don't ship it like this
|
||||||
|
UpdateType::None => Some(tag),
|
||||||
|
UpdateType::Major => Some(tag).filter(|tag| {
|
||||||
|
base.as_standard().unwrap().major == tag.as_standard().unwrap().major
|
||||||
|
}),
|
||||||
|
UpdateType::Minor => Some(tag).filter(|tag| {
|
||||||
|
base.as_standard().unwrap().major == tag.as_standard().unwrap().major
|
||||||
|
&& base.as_standard().unwrap().minor
|
||||||
|
== tag.as_standard().unwrap().minor
|
||||||
|
}),
|
||||||
|
UpdateType::Patch => Some(tag).filter(|tag| {
|
||||||
|
base.as_standard().unwrap().major == tag.as_standard().unwrap().major
|
||||||
|
&& base.as_standard().unwrap().minor
|
||||||
|
== tag.as_standard().unwrap().minor
|
||||||
|
&& base.as_standard().unwrap().patch
|
||||||
|
== tag.as_standard().unwrap().patch
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
_ => false,
|
|
||||||
})
|
|
||||||
.filter_map(|(tag, _)| match ctx.config.ignore_update_type {
|
|
||||||
UpdateType::None => Some(tag),
|
|
||||||
UpdateType::Major => Some(tag).filter(|tag| base.major == tag.major),
|
|
||||||
UpdateType::Minor => {
|
|
||||||
Some(tag).filter(|tag| base.major == tag.major && base.minor == tag.minor)
|
|
||||||
}
|
|
||||||
UpdateType::Patch => Some(tag).filter(|tag| {
|
|
||||||
base.major == tag.major
|
|
||||||
&& base.minor == tag.minor
|
|
||||||
&& base.patch == tag.patch
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
.dedup()
|
.dedup()
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::{
|
|||||||
error,
|
error,
|
||||||
http::Client,
|
http::Client,
|
||||||
registry::{get_latest_digest, get_latest_tag},
|
registry::{get_latest_digest, get_latest_tag},
|
||||||
structs::{status::Status, version::Version},
|
structs::{standard_version::StandardVersionPart, status::Status, update::Update, version::Version},
|
||||||
utils::reference::split,
|
utils::reference::split,
|
||||||
Context,
|
Context,
|
||||||
};
|
};
|
||||||
@@ -10,35 +10,35 @@ use crate::{
|
|||||||
use super::{
|
use super::{
|
||||||
inspectdata::InspectData,
|
inspectdata::InspectData,
|
||||||
parts::Parts,
|
parts::Parts,
|
||||||
update::{DigestUpdateInfo, Update, UpdateInfo, UpdateResult, VersionUpdateInfo},
|
update::{DigestUpdateInfo, UpdateResult, VersionUpdateInfo},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
/// Any local information about the image
|
||||||
|
#[derive(Clone, Default)]
|
||||||
#[cfg_attr(test, derive(Debug))]
|
#[cfg_attr(test, derive(Debug))]
|
||||||
pub struct DigestInfo {
|
pub struct Info {
|
||||||
pub local_digests: Vec<String>,
|
pub local_digests: Vec<String>,
|
||||||
pub remote_digest: Option<String>,
|
pub version: Version,
|
||||||
|
pub url: Option<String>,
|
||||||
|
pub used_by: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
/// Any new information obtained about the image
|
||||||
#[cfg_attr(test, derive(Debug))]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct VersionInfo {
|
pub struct UpdateInfo {
|
||||||
pub current_tag: Version,
|
pub remote_digest: Option<String>,
|
||||||
pub latest_remote_tag: Option<Version>,
|
pub latest_version: Option<Version>
|
||||||
pub format_str: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Image struct that contains all information that may be needed by a function working with an image.
|
/// 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
|
/// It's designed to be passed around between functions
|
||||||
#[derive(Clone, PartialEq, Default)]
|
#[derive(Clone, Default)]
|
||||||
#[cfg_attr(test, derive(Debug))]
|
#[cfg_attr(test, derive(Debug))]
|
||||||
pub struct Image {
|
pub struct Image {
|
||||||
pub reference: String,
|
pub reference: String,
|
||||||
pub parts: Parts,
|
pub parts: Parts,
|
||||||
pub url: Option<String>,
|
pub info: Info,
|
||||||
pub digest_info: Option<DigestInfo>,
|
pub update_info: UpdateInfo,
|
||||||
pub version_info: Option<VersionInfo>,
|
|
||||||
pub used_by: Vec<String>,
|
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
pub time_ms: u32,
|
pub time_ms: u32,
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ impl Image {
|
|||||||
return None; // As far as I know, references that contain @ are either manually pulled by the user or automatically created because of swarm. In the first case AFAICT we can't know what tag was originally pulled, so we'd have to make assumptions and I've decided to remove this. The other case is already handled seperately, so this also ensures images aren't displayed twice, once with and once without a digest.
|
return None; // As far as I know, references that contain @ are either manually pulled by the user or automatically created because of swarm. In the first case AFAICT we can't know what tag was originally pulled, so we'd have to make assumptions and I've decided to remove this. The other case is already handled seperately, so this also ensures images aren't displayed twice, once with and once without a digest.
|
||||||
};
|
};
|
||||||
let (registry, repository, tag) = split(&reference);
|
let (registry, repository, tag) = split(&reference);
|
||||||
let version_tag = Version::from_tag(&tag);
|
let version_tag = Version::from(&tag, ctx.config.images.get(&reference).map(|cfg| &cfg.tag_type));
|
||||||
let local_digests = digests
|
let local_digests = digests
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(
|
.filter_map(
|
||||||
@@ -77,16 +77,7 @@ impl Image {
|
|||||||
repository,
|
repository,
|
||||||
tag,
|
tag,
|
||||||
},
|
},
|
||||||
url: image.url(),
|
info: Info { local_digests, version: version_tag, url: image.url(), used_by: Vec::new() },
|
||||||
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()
|
..Default::default()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -95,28 +86,24 @@ impl Image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
/// 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 {
|
pub fn from_reference(reference: &str, ctx: &Context) -> Self {
|
||||||
let (registry, repository, tag) = split(reference);
|
let (registry, repository, tag) = split(reference);
|
||||||
let version_tag = Version::from_tag(&tag);
|
let version_tag = Version::from(&tag, ctx.config.images.get(reference).map(|cfg| &cfg.tag_type));
|
||||||
match version_tag {
|
match version_tag {
|
||||||
Some((version, format_str)) => Self {
|
Version::Unknown => error!(
|
||||||
|
"Image {} is not available locally and does not have a recognizable tag format!",
|
||||||
|
reference
|
||||||
|
),
|
||||||
|
v => Self {
|
||||||
reference: reference.to_string(),
|
reference: reference.to_string(),
|
||||||
parts: Parts {
|
parts: Parts {
|
||||||
registry,
|
registry,
|
||||||
repository,
|
repository,
|
||||||
tag,
|
tag,
|
||||||
},
|
},
|
||||||
version_info: Some(VersionInfo {
|
info: Info { local_digests: Vec::new(), version: v, url: None, used_by: Vec::new() },
|
||||||
current_tag: version,
|
|
||||||
format_str,
|
|
||||||
latest_remote_tag: None,
|
|
||||||
}),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
None => error!(
|
|
||||||
"Image {} is not available locally and does not have a recognizable tag format!",
|
|
||||||
reference
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,25 +111,18 @@ impl Image {
|
|||||||
if self.error.is_some() {
|
if self.error.is_some() {
|
||||||
Status::Unknown(self.error.clone().unwrap())
|
Status::Unknown(self.error.clone().unwrap())
|
||||||
} else {
|
} else {
|
||||||
match &self.version_info {
|
match self.update_info.latest_version {
|
||||||
Some(data) => data
|
Some(latest_version) => latest_version.to_status(self.info.version),
|
||||||
.latest_remote_tag
|
None => match self.update_info.remote_digest {
|
||||||
.as_ref()
|
Some(remote_digest) => {
|
||||||
.unwrap()
|
if self.info.local_digests.contains(&remote_digest) {
|
||||||
.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
|
Status::UpToDate
|
||||||
} else {
|
} else {
|
||||||
Status::UpdateAvailable
|
Status::UpdateAvailable
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
None => unreachable!(), // I hope?
|
None => unreachable!() // I hope?
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,20 +138,14 @@ impl Image {
|
|||||||
Update {
|
Update {
|
||||||
reference: self.reference.clone(),
|
reference: self.reference.clone(),
|
||||||
parts: self.parts.clone(),
|
parts: self.parts.clone(),
|
||||||
url: self.url.clone(),
|
url: self.info.url.clone(),
|
||||||
result: UpdateResult {
|
result: UpdateResult {
|
||||||
has_update: has_update.to_option_bool(),
|
has_update: has_update.to_option_bool(),
|
||||||
info: match has_update {
|
info: match has_update {
|
||||||
Status::Unknown(_) => UpdateInfo::None,
|
Status::Unknown(_) => crate::structs::update::UpdateInfo::None,
|
||||||
_ => match update_type {
|
_ => match update_type {
|
||||||
"version" => {
|
"version" => {
|
||||||
let (new_tag, format_str) = match &self.version_info {
|
let update_info = &self.update_info.latest_version.unwrap().as_standard().unwrap();
|
||||||
Some(data) => (
|
|
||||||
data.latest_remote_tag.clone().unwrap(),
|
|
||||||
data.format_str.clone(),
|
|
||||||
),
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
UpdateInfo::Version(VersionUpdateInfo {
|
UpdateInfo::Version(VersionUpdateInfo {
|
||||||
version_update_type: match has_update {
|
version_update_type: match has_update {
|
||||||
@@ -181,12 +155,12 @@ impl Image {
|
|||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
.to_string(),
|
.to_string(),
|
||||||
new_tag: format_str
|
new_tag: update_info.format_str
|
||||||
.replacen("{}", &new_tag.major.to_string(), 1)
|
.replacen("{}", &update_info.major.to_string(), 1)
|
||||||
.replacen("{}", &new_tag.minor.unwrap_or(0).to_string(), 1)
|
.replacen("{}", &update_info.minor.unwrap_or_default().to_string(), 1)
|
||||||
.replacen("{}", &new_tag.patch.unwrap_or(0).to_string(), 1),
|
.replacen("{}", &update_info.patch.unwrap_or_default().to_string(), 1),
|
||||||
// Throwing these in, because they're useful for the CLI output, however we won't (de)serialize them
|
// Throwing these in, because they're useful for the CLI output, however we won't (de)serialize them
|
||||||
current_version: self
|
current_version: self.info.version.as_standard().unwrap().to_string()
|
||||||
.version_info
|
.version_info
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ pub mod parts;
|
|||||||
pub mod status;
|
pub mod status;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
|
pub mod standard_version;
|
||||||
180
src/structs/standard_version.rs
Normal file
180
src/structs/standard_version.rs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
use std::{cmp::Ordering, fmt::Display};
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug, Default)] // Default is so I can avoid constructing a struct every time I want to use a version number of 0 as a default.
|
||||||
|
pub struct StandardVersionPart {
|
||||||
|
value: u32,
|
||||||
|
length: u8, // If the value is prefixed by zeroes, the total length, otherwise 0
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StandardVersionPart {
|
||||||
|
fn from_split(split: &str) -> Self {
|
||||||
|
if split.len() == 1 && split == "0" {
|
||||||
|
Self::default()
|
||||||
|
} else {
|
||||||
|
Self {
|
||||||
|
value: split.parse().expect("Expected number to be less than 2^32"), // Unwrapping is safe, because we've verified that the string consists of digits and we don't care about supporting big numbers.
|
||||||
|
length: {
|
||||||
|
if split.starts_with('0') {
|
||||||
|
split.len() as u8 // We're casting the zeroes to u8, because no sane person uses more than 255 zeroes as a version prefix. Oh wait, tags can't even be that long
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for StandardVersionPart {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
if self.length == other.length {
|
||||||
|
self.value.partial_cmp(&other.value)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for StandardVersionPart {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_fmt(format_args!(
|
||||||
|
"{:0<zeroes$}",
|
||||||
|
self.value,
|
||||||
|
zeroes = self.length as usize
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a semver-like version.
|
||||||
|
/// While not conforming to the SemVer standard, but was designed to handle common versioning schemes across a wide range of Docker images.
|
||||||
|
/// Minor and patch versions are considered optional.
|
||||||
|
/// Matching happens with a regex.
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub struct StandardVersion {
|
||||||
|
pub major: StandardVersionPart,
|
||||||
|
pub minor: Option<StandardVersionPart>,
|
||||||
|
pub patch: Option<StandardVersionPart>,
|
||||||
|
pub format_str: String, // The tag with {} in the place the version was matched.
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StandardVersion {
|
||||||
|
/// Tries to extract a semver-like version from a tag.
|
||||||
|
/// Returns a Result<StandardVersion, ()> indicating whether parsing succeeded
|
||||||
|
pub fn from_tag(tag: &str) -> Result<Self, ()> {
|
||||||
|
/// 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-9]+)(?:\.(?P<minor>[0-9]*))?(?:\.(?P<patch>[0-9]*))?")
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
let mut captures = VERSION_REGEX.captures_iter(tag);
|
||||||
|
// And now... terrible best match selection for everyone! Actually, it's probably not that terrible. I don't know.
|
||||||
|
match captures.next() {
|
||||||
|
Some(mut best_match) => {
|
||||||
|
let mut max_matches: u8 = 0; // Why does Rust not have `u2`s?
|
||||||
|
for capture in captures {
|
||||||
|
let count = capture.iter().filter_map(|c| c).count() as u8;
|
||||||
|
if count > max_matches {
|
||||||
|
max_matches = count;
|
||||||
|
best_match = capture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let start_pos;
|
||||||
|
let mut end_pos;
|
||||||
|
let major: StandardVersionPart = match best_match.name("major") {
|
||||||
|
Some(major) => {
|
||||||
|
start_pos = major.start();
|
||||||
|
end_pos = major.end();
|
||||||
|
StandardVersionPart::from_split(major.as_str())
|
||||||
|
}
|
||||||
|
None => return Err(()),
|
||||||
|
};
|
||||||
|
let minor: Option<StandardVersionPart> = best_match.name("minor").map(|minor| {
|
||||||
|
end_pos = minor.end();
|
||||||
|
StandardVersionPart::from_split(minor.as_str())
|
||||||
|
});
|
||||||
|
let patch: Option<StandardVersionPart> = best_match.name("patch").map(|patch| {
|
||||||
|
end_pos = patch.end();
|
||||||
|
StandardVersionPart::from_split(patch.as_str())
|
||||||
|
});
|
||||||
|
let mut format_str = tag.to_string();
|
||||||
|
format_str.replace_range(start_pos..end_pos, "{}");
|
||||||
|
Ok(Self {
|
||||||
|
major,
|
||||||
|
minor,
|
||||||
|
patch,
|
||||||
|
format_str,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for StandardVersion {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
if self.format_str != other.format_str {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
match self.major.partial_cmp(&other.major) {
|
||||||
|
Some(ordering) => match ordering {
|
||||||
|
Ordering::Equal => match self.minor.partial_cmp(&other.minor) {
|
||||||
|
Some(ordering) => match ordering {
|
||||||
|
Ordering::Equal => self.patch.partial_cmp(&other.patch),
|
||||||
|
_ => Some(ordering),
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
_ => Some(ordering),
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn standard_version() {
|
||||||
|
assert_eq!(StandardVersion::from_tag("5.3.2"), Ok(StandardVersion { major: StandardVersionPart { value: 5, length: 0 }, minor: Some(StandardVersionPart { value: 3, length: 0 }), patch: Some(StandardVersionPart { value: 2, length: 0 }) , format_str: String::from("{}") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("14"), Ok(StandardVersion { major: StandardVersionPart { value: 14, length: 0 }, minor: None, patch: None , format_str: String::from("{}") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("v0.107.53"), Ok(StandardVersion { major: StandardVersionPart { value: 0, length: 0 }, minor: Some(StandardVersionPart { value: 107, length: 0 }), patch: Some(StandardVersionPart { value: 53, length: 0 }) , format_str: String::from("v{}") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("12-alpine"), Ok(StandardVersion { major: StandardVersionPart { value: 12, length: 0 }, minor: None, patch: None , format_str: String::from("{}-alpine") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("0.9.5-nginx"), Ok(StandardVersion { major: StandardVersionPart { value: 0, length: 0 }, minor: Some(StandardVersionPart { value: 9, length: 0 }), patch: Some(StandardVersionPart { value: 5, length: 0 }) , format_str: String::from("{}-nginx") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("v27.0"), Ok(StandardVersion { major: StandardVersionPart { value: 27, length: 0 }, minor: Some(StandardVersionPart { value: 0, length: 0 }), patch: None , format_str: String::from("v{}") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("16.1"), Ok(StandardVersion { major: StandardVersionPart { value: 16, length: 0 }, minor: Some(StandardVersionPart { value: 1, length: 0 }), patch: None , format_str: String::from("{}") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("version-1.5.6"), Ok(StandardVersion { major: StandardVersionPart { value: 1, length: 0 }, minor: Some(StandardVersionPart { value: 5, length: 0 }), patch: Some(StandardVersionPart { value: 6, length: 0 }) , format_str: String::from("version-{}") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("15.4-alpine"), Ok(StandardVersion { major: StandardVersionPart { value: 15, length: 0 }, minor: Some(StandardVersionPart { value: 4, length: 0 }), patch: None , format_str: String::from("{}-alpine") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("pg14-v0.2.0"), Ok(StandardVersion { major: StandardVersionPart { value: 0, length: 0 }, minor: Some(StandardVersionPart { value: 2, length: 0 }), patch: Some(StandardVersionPart { value: 0, length: 0 }) , format_str: String::from("pg14-v{}") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("18-jammy-full.s6-v0.88.0"), Ok(StandardVersion { major: StandardVersionPart { value: 0, length: 0 }, minor: Some(StandardVersionPart { value: 88, length: 0 }), patch: Some(StandardVersionPart { value: 0, length: 0 }) , format_str: String::from("18-jammy-full.s6-v{}") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("fpm-2.1.0-prod"), Ok(StandardVersion { major: StandardVersionPart { value: 2, length: 0 }, minor: Some(StandardVersionPart { value: 1, length: 0 }), patch: Some(StandardVersionPart { value: 0, length: 0 }) , format_str: String::from("fpm-{}-prod") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("7.3.3.50"), Ok(StandardVersion { major: StandardVersionPart { value: 7, length: 0 }, minor: Some(StandardVersionPart { value: 3, length: 0 }), patch: Some(StandardVersionPart { value: 3, length: 0 }) , format_str: String::from("{}.50") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("1.21.11-0"), Ok(StandardVersion { major: StandardVersionPart { value: 1, length: 0 }, minor: Some(StandardVersionPart { value: 21, length: 0 }), patch: Some(StandardVersionPart { value: 11, length: 0 }) , format_str: String::from("{}-0") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("4.1.2.1-full"), Ok(StandardVersion { major: StandardVersionPart { value: 4, length: 0 }, minor: Some(StandardVersionPart { value: 1, length: 0 }), patch: Some(StandardVersionPart { value: 2, length: 0 }) , format_str: String::from("{}.1-full") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("v4.0.3-ls215"), Ok(StandardVersion { major: StandardVersionPart { value: 4, length: 0 }, minor: Some(StandardVersionPart { value: 0, length: 0 }), patch: Some(StandardVersionPart { value: 3, length: 0 }) , format_str: String::from("v{}-ls215") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("24.04.11.2.1"), Ok(StandardVersion { major: StandardVersionPart { value: 24, length: 0 }, minor: Some(StandardVersionPart { value: 4, length: 2 }), patch: Some(StandardVersionPart { value: 11, length: 0 }) , format_str: String::from("{}.2.1") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("example15-test"), Ok(StandardVersion { major: StandardVersionPart { value: 15, length: 0 }, minor: None, patch: None , format_str: String::from("example{}-test") }));
|
||||||
|
assert_eq!(StandardVersion::from_tag("watch-the-dot-5.3.2.careful"), Ok(StandardVersion { major: StandardVersionPart { value: 5, length: 0 }, minor: Some(StandardVersionPart { value: 3, length: 0 }), patch: Some(StandardVersionPart { value: 2, length: 0 }), format_str: String::from("watch-the-dot-{}.careful") }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_part() {
|
||||||
|
assert_eq!(
|
||||||
|
format!(
|
||||||
|
"{:?}",
|
||||||
|
StandardVersionPart {
|
||||||
|
value: 21,
|
||||||
|
length: 4
|
||||||
|
}
|
||||||
|
),
|
||||||
|
String::from("0021")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,14 +24,14 @@ pub struct UpdateResult {
|
|||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||||
#[cfg_attr(test, derive(PartialEq, Default))]
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum UpdateInfo {
|
pub enum UpdateInfo {
|
||||||
#[cfg_attr(test, default)]
|
#[default]
|
||||||
None,
|
None,
|
||||||
Version(VersionUpdateInfo),
|
Version(VersionUpdateInfo),
|
||||||
Digest(DigestUpdateInfo),
|
Digest(String), // Remote digest
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, Debug)]
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
|
|||||||
@@ -1,188 +1,63 @@
|
|||||||
use std::{cmp::Ordering, fmt::Display};
|
use crate::{config::TagType, structs::standard_version::StandardVersion};
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
#[derive(Clone, Default, PartialEq, Debug)]
|
||||||
use regex::Regex;
|
#[non_exhaustive]
|
||||||
|
pub enum Version {
|
||||||
use super::status::Status;
|
#[default]
|
||||||
|
Unknown,
|
||||||
/// Semver-like version struct
|
Semver(StandardVersion),
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub struct Version {
|
|
||||||
pub major: u32,
|
|
||||||
pub minor: Option<u32>,
|
|
||||||
pub patch: Option<u32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Version {
|
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_standard(tag: &str) -> Result<Self, ()> {
|
||||||
pub fn from_tag(tag: &str) -> Option<(Self, String)> {
|
match StandardVersion::from_tag(tag) {
|
||||||
/// 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.
|
Ok(version) => Ok(Version::Semver(version)),
|
||||||
static VERSION_REGEX: Lazy<Regex> = Lazy::new(|| {
|
Err(e) => Err(e),
|
||||||
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 {
|
pub fn format_string(&self) -> Option<String> {
|
||||||
match self.major.cmp(&base.major) {
|
match self {
|
||||||
Ordering::Greater => Status::UpdateMajor,
|
Self::Semver(v) => Some(v.format_str.clone()),
|
||||||
Ordering::Equal => match (self.minor, base.minor) {
|
Self::Unknown => None,
|
||||||
(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 {
|
pub fn from(tag: &str, tag_type: Option<&TagType>) -> Self {
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
match tag_type {
|
||||||
let major_ordering = self.major.cmp(&other.major);
|
Some(t) => match t {
|
||||||
match major_ordering {
|
TagType::Standard => Self::from_standard(tag).unwrap_or(Self::Unknown),
|
||||||
Ordering::Equal => match (self.minor, other.minor) {
|
TagType::Extended => unimplemented!(),
|
||||||
(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,
|
None => match Self::from_standard(tag) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => Self::Unknown, // match self.from_...
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn r#type(&self) -> Option<TagType> {
|
||||||
|
match self {
|
||||||
|
Self::Semver(_) => Some(TagType::Standard),
|
||||||
|
Self::Unknown => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_standard(&self) -> Option<&StandardVersion> {
|
||||||
|
match self {
|
||||||
|
Self::Semver(s) => Some(s),
|
||||||
|
_ => None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for Version {
|
impl PartialOrd for Version {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
Some(self.cmp(other))
|
match (self, other) {
|
||||||
}
|
(Self::Unknown, Self::Unknown)
|
||||||
}
|
| (Self::Unknown, Self::Semver(_))
|
||||||
|
| (Self::Semver(_), Self::Unknown) => None, // Could also just implement the other arms first and leave this as _, but better be explicit rather than implicit
|
||||||
impl Display for Version {
|
(Self::Semver(a), Self::Semver(b)) => a.partial_cmp(b),
|
||||||
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" ))));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user