use anyhow::{Context, Result, bail};
pub const MINIMUM: i32 = 30;
pub const LATEST: i32 = 35;
pub fn format_version(minor: i32) -> String {
format!("1.{minor}")
}
pub fn check_version_supported(cluster_version: &str) -> Result<Option<i32>> {
let current_minor = parse_minor(cluster_version)?;
if current_minor < MINIMUM {
bail!(
"Cluster version {cluster_version} is below the minimum supported version ({}). \
Please upgrade to at least {} before using this tool.",
format_version(MINIMUM),
format_version(MINIMUM),
);
}
if current_minor >= LATEST {
return Ok(None);
}
Ok(Some(current_minor + 1))
}
pub fn get_target_version(current_version: &str) -> Result<i32> {
let current_minor = parse_minor(current_version)?;
if current_minor < MINIMUM {
bail!(
"Cluster version {current_version} is below the minimum supported version ({}). \
Please upgrade to at least {} before using this tool.",
format_version(MINIMUM),
format_version(MINIMUM),
);
}
if current_minor >= LATEST {
bail!(
"Cluster is already on the latest supported version ({})",
format_version(LATEST),
);
}
Ok(current_minor + 1)
}
pub fn validate_target_version(target: &str, cluster_version: &str) -> Result<i32> {
let target_str = target.strip_prefix('v').unwrap_or(target);
let target_minor = if target_str.contains('.') {
parse_minor(target_str)?
} else {
target_str.parse::<i32>().context(format!(
"Invalid target version '{target}', expected format like '1.34' or '34'"
))?
};
let current_minor = parse_minor(cluster_version)?;
if target_minor <= current_minor {
bail!(
"Target version {} must be greater than current cluster version {}",
format_version(target_minor),
format_version(current_minor),
);
}
if target_minor > LATEST {
bail!(
"Target version {} exceeds the latest supported version ({})",
format_version(target_minor),
format_version(LATEST),
);
}
Ok(target_minor)
}
pub fn parse_minor(version: &str) -> Result<i32> {
let parts: Vec<&str> = version.split('.').collect();
let minor_str = parts
.get(1)
.context(format!("Invalid version format '{version}', expected 'X.Y[.Z]'"))?;
let minor = minor_str.parse::<i32>()?;
Ok(minor)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_minor_valid_versions() {
let cases = vec![
("v1.20.7-eks-123456", 20),
("1.30", 30),
("v1.30", 30),
("v1.30.0-eks-12345", 30),
("1.25.3", 25),
];
for (input, expected) in cases {
let result = parse_minor(input).unwrap();
assert_eq!(result, expected, "parse_minor({input})");
}
}
#[test]
fn parse_minor_invalid_versions() {
assert!(parse_minor("125").is_err(), "should fail on '125' (no dot)");
assert!(parse_minor("").is_err(), "should fail on empty string");
}
#[test]
fn format_version_formats_correctly() {
assert_eq!(format_version(30), "1.30");
assert_eq!(format_version(35), "1.35");
}
#[test]
fn get_target_version_increments_minor() {
let result = get_target_version(&format_version(MINIMUM)).unwrap();
assert_eq!(result, MINIMUM + 1);
}
#[test]
fn get_target_version_errors_on_latest() {
let result = get_target_version(&format_version(LATEST));
assert!(result.is_err(), "should error when already on LATEST");
}
#[test]
fn get_target_version_errors_below_minimum() {
let result = get_target_version("1.29");
assert!(result.is_err(), "should error when below MINIMUM");
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("below the minimum supported version"),
"error message: {msg}"
);
}
#[test]
fn get_target_version_at_minimum() {
let result = get_target_version(&format_version(MINIMUM));
assert!(result.is_ok(), "should succeed at MINIMUM");
}
#[test]
fn check_version_supported_below_minimum() {
let result = check_version_supported("1.29");
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("below the minimum"), "error message: {msg}");
}
#[test]
fn check_version_supported_at_latest() {
let result = check_version_supported(&format_version(LATEST)).unwrap();
assert!(result.is_none(), "should return None at latest");
}
#[test]
fn check_version_supported_upgradeable() {
let result = check_version_supported(&format_version(MINIMUM)).unwrap();
assert_eq!(result, Some(MINIMUM + 1));
}
#[test]
fn check_version_supported_one_below_latest() {
let result = check_version_supported(&format_version(LATEST - 1)).unwrap();
assert_eq!(result, Some(LATEST));
}
#[test]
fn check_version_supported_above_latest() {
let result = check_version_supported(&format_version(LATEST + 1)).unwrap();
assert!(result.is_none(), "should return None above latest");
}
#[test]
fn format_version_edge_cases() {
assert_eq!(format_version(0), "1.0");
assert_eq!(format_version(99), "1.99");
}
#[test]
fn parse_minor_with_v_prefix() {
assert_eq!(parse_minor("v1.30").unwrap(), 30);
assert_eq!(parse_minor("v1.30.0-eks-12345").unwrap(), 30);
}
#[test]
fn validate_target_version_single_hop() {
let result = validate_target_version("1.31", "1.30").unwrap();
assert_eq!(result, 31);
}
#[test]
fn validate_target_version_multi_hop() {
let result = validate_target_version("1.33", "1.30").unwrap();
assert_eq!(result, 33);
}
#[test]
fn validate_target_version_with_v_prefix() {
let result = validate_target_version("v1.33", "1.30").unwrap();
assert_eq!(result, 33);
}
#[test]
fn validate_target_version_below_current() {
let result = validate_target_version("1.29", "1.30");
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("must be greater than"), "error: {msg}");
}
#[test]
fn validate_target_version_equal_current() {
let result = validate_target_version("1.30", "1.30");
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("must be greater than"), "error: {msg}");
}
#[test]
fn validate_target_version_above_latest() {
let result = validate_target_version(&format_version(LATEST + 1), "1.30");
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("exceeds the latest supported version"), "error: {msg}");
}
#[test]
fn validate_target_version_at_latest() {
let result = validate_target_version(&format_version(LATEST), &format_version(LATEST - 1)).unwrap();
assert_eq!(result, LATEST);
}
}