rusta-cli 1.2.7

macOS arm64 CLI for creating and managing Ubuntu VMs on Tart
Documentation
use std::io::IsTerminal;
use std::time::Duration;

use serde_json::Value;

use crate::error::{Error, Result};

const DEFAULT_VERSION: &str = "24.04";
const TOKEN_URL: &str = "https://ghcr.io/token?scope=repository:cirruslabs/ubuntu:pull";
const TAGS_URL: &str = "https://ghcr.io/v2/cirruslabs/ubuntu/tags/list";

fn token_url() -> String {
    std::env::var("RUSTA_GHCR_TOKEN_URL").unwrap_or_else(|_| TOKEN_URL.to_string())
}

fn tags_url() -> String {
    std::env::var("RUSTA_GHCR_TAGS_URL").unwrap_or_else(|_| TAGS_URL.to_string())
}

pub fn run() -> Result<u8> {
    let agent = ureq::AgentBuilder::new()
        .timeout(Duration::from_secs(30))
        .build();

    let token_resp: Value = agent
        .get(&token_url())
        .call()
        .map_err(|e| Error::msg(format!("ghcr.io token request failed: {e}")))?
        .into_json()
        .map_err(|e| Error::msg(format!("ghcr.io token response parse: {e}")))?;
    let token = token_resp
        .get("token")
        .and_then(Value::as_str)
        .ok_or_else(|| Error::msg("ghcr.io did not return a pull token".to_string()))?
        .to_string();

    let tags_resp: Value = agent
        .get(&tags_url())
        .set("Authorization", &format!("Bearer {token}"))
        .call()
        .map_err(|e| Error::msg(format!("ghcr.io tags request failed: {e}")))?
        .into_json()
        .map_err(|e| Error::msg(format!("ghcr.io tags response parse: {e}")))?;
    let tags = tags_resp
        .get("tags")
        .and_then(Value::as_array)
        .ok_or_else(|| Error::msg("ghcr.io tags response missing `tags` array".to_string()))?;

    let mut versions: Vec<(u32, u32, String)> = tags
        .iter()
        .filter_map(Value::as_str)
        .filter_map(parse_version)
        .collect();
    versions.sort();

    let tty = std::io::stdout().is_terminal();
    let (green, reset) = if tty {
        ("\x1b[0;32m", "\x1b[0m")
    } else {
        ("", "")
    };

    for (_, _, v) in &versions {
        if v == DEFAULT_VERSION {
            println!("{green}{v}{reset} (default)");
        } else {
            println!("{v}");
        }
    }
    Ok(0)
}

pub(crate) fn parse_version(t: &str) -> Option<(u32, u32, String)> {
    let (a, b) = t.split_once('.')?;
    let major: u32 = a.parse().ok()?;
    let minor: u32 = b.parse().ok()?;
    if a.chars().all(|c| c.is_ascii_digit()) && b.chars().all(|c| c.is_ascii_digit()) {
        Some((major, minor, t.to_string()))
    } else {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_version_accepts_two_dot() {
        assert_eq!(parse_version("24.04"), Some((24, 4, "24.04".into())));
        assert_eq!(parse_version("22.10"), Some((22, 10, "22.10".into())));
    }

    #[test]
    fn parse_version_rejects_non_numeric() {
        assert!(parse_version("latest").is_none());
        assert!(parse_version("24").is_none());
        assert!(parse_version("24.04.1").is_none());
        assert!(parse_version("24.x").is_none());
    }
}