mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-17 17:43:37 -05:00
V3
Many many many changes, honestly just read the release notes
This commit is contained in:
221
src/structs/image.rs
Normal file
221
src/structs/image.rs
Normal 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!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/structs/inspectdata.rs
Normal file
26
src/structs/inspectdata.rs
Normal 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
6
src/structs/mod.rs
Normal 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
8
src/structs/parts.rs
Normal 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
42
src/structs/status.rs
Normal 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
105
src/structs/update.rs
Normal 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
182
src/structs/version.rs
Normal 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" ))));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user