mirror of
https://github.com/vhaudiquet/ecrou.git
synced 2026-11-12 22:22:44 +00:00
feat(distro): ubuntu-base from real releases, codename and number aliased
Ubuntu tarballs came from the daily/current directories - moving targets, no published checksums guarantee, gone when a release EOLs. resolve_ubuntu_url now builds the cdimage release URL: ubuntu-base/releases/<YY.MM>/release/ubuntu-base-<YY.MM>-base-<arch>.tar.gz, whose SHA256SUMS the existing checksum verification consumes. Resolution returns the YY.MM number (the cache key follows), and accepts the codename interchangeably: a static table maps the known codenames offline - ubuntu:resolute and ubuntu:26.04 are the same image and the same entry - with meta-release as a fallback for codenames newer than the table. Floating aliases (latest/lts) still consult meta-release; explicit numbers never touch the network, which also removes the per-cache-path network lookup generate_cache_filename used to trigger for codename inputs. URLs verified live: tarball and SHA256SUMS both 200.
This commit is contained in:
1 parent
28f24abb6f
commit
73eaeb57ac
2 files changed
+176
-89
No files matched your search
+167
-85
@@ -250,34 +250,129 @@ pub fn checksum_artifact_url(distro: &Distro, tarball_url: &str) -> Option<Strin
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the latest Ubuntu codename from a changelogs.ubuntu.com meta-release file.
|
||||
/// Pass the LTS-only URL to get the latest LTS, or the full URL for the latest release.
|
||||
fn fetch_ubuntu_codename(meta_release_url: &str) -> anyhow::Result<String> {
|
||||
let client = crate::download::http_client(Some(Duration::from_secs(30)))?;
|
||||
let text = client
|
||||
.get(meta_release_url)
|
||||
.send()
|
||||
.with_context(|| format!("Failed to fetch {}", meta_release_url))?
|
||||
.text()
|
||||
.with_context(|| format!("Failed to read {}", meta_release_url))?;
|
||||
/// Ubuntu releases with base images: `(codename, YY.MM)`, oldest
|
||||
/// first. Static on purpose — `ubuntu:26.04` and `ubuntu:resolute`
|
||||
/// must resolve identically and offline, and a codename never changes
|
||||
/// meaning. A newer release than this table needs an entry only for
|
||||
/// its codename; its number always resolves without one.
|
||||
const UBUNTU_CODENAMES: &[(&str, &str)] = &[
|
||||
("xenial", "16.04"),
|
||||
("bionic", "18.04"),
|
||||
("cosmic", "18.10"),
|
||||
("disco", "19.04"),
|
||||
("eoan", "19.10"),
|
||||
("focal", "20.04"),
|
||||
("groovy", "20.10"),
|
||||
("hirsute", "21.04"),
|
||||
("impish", "21.10"),
|
||||
("jammy", "22.04"),
|
||||
("kinetic", "22.10"),
|
||||
("lunar", "23.04"),
|
||||
("mantic", "23.10"),
|
||||
("noble", "24.04"),
|
||||
("oracular", "24.10"),
|
||||
("plucky", "25.04"),
|
||||
("questing", "25.10"),
|
||||
("resolute", "26.04"),
|
||||
];
|
||||
|
||||
let mut current_dist: Option<String> = None;
|
||||
let mut latest: Option<String> = None;
|
||||
/// One entry of a meta-release file.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct UbuntuRelease {
|
||||
codename: String,
|
||||
number: String,
|
||||
supported: bool,
|
||||
}
|
||||
|
||||
/// Parse a changelogs.ubuntu.com meta-release body into releases, in
|
||||
/// file order (oldest first).
|
||||
fn parse_ubuntu_meta_release(text: &str) -> Vec<UbuntuRelease> {
|
||||
let mut releases = Vec::new();
|
||||
let mut current: Option<(String, String, bool)> = None;
|
||||
|
||||
fn flush(current: &mut Option<(String, String, bool)>, releases: &mut Vec<UbuntuRelease>) {
|
||||
if let Some((codename, number, supported)) = current.take() {
|
||||
if !number.is_empty() {
|
||||
releases.push(UbuntuRelease {
|
||||
codename,
|
||||
number,
|
||||
supported,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for line in text.lines() {
|
||||
if let Some(dist) = line.strip_prefix("Dist: ") {
|
||||
current_dist = Some(dist.trim().to_string());
|
||||
flush(&mut current, &mut releases);
|
||||
current = Some((dist.trim().to_string(), String::new(), false));
|
||||
} else if let Some(version) = line.strip_prefix("Version: ") {
|
||||
// "22.04", "22.04 LTS", "24.04.1 LTS" — normalise to YY.MM,
|
||||
// the granularity ubuntu-base publishes.
|
||||
let raw = version.split_whitespace().next().unwrap_or("");
|
||||
if let Some((_, number, _)) = current.as_mut() {
|
||||
*number = normalise_ubuntu_number(raw);
|
||||
}
|
||||
} else if line.trim_start().starts_with("Supported: 1") {
|
||||
if let Some(dist) = current_dist.take() {
|
||||
latest = Some(dist);
|
||||
if let Some((_, _, supported)) = current.as_mut() {
|
||||
*supported = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
flush(&mut current, &mut releases);
|
||||
releases
|
||||
}
|
||||
|
||||
latest.ok_or_else(|| {
|
||||
/// Fetch a meta-release file and return its releases.
|
||||
fn fetch_ubuntu_meta_releases(url: &str) -> anyhow::Result<Vec<UbuntuRelease>> {
|
||||
let client = crate::download::http_client(Some(Duration::from_secs(30)))?;
|
||||
let text = client
|
||||
.get(url)
|
||||
.send()
|
||||
.with_context(|| format!("Failed to fetch {}", url))?
|
||||
.text()
|
||||
.with_context(|| format!("Failed to read {}", url))?;
|
||||
Ok(parse_ubuntu_meta_release(&text))
|
||||
}
|
||||
|
||||
/// The YY.MM number of the newest supported release in a meta-release file.
|
||||
/// Pass the LTS-only URL for the latest LTS, the full URL for the latest
|
||||
/// release.
|
||||
fn fetch_ubuntu_latest_number(url: &str) -> anyhow::Result<String> {
|
||||
fetch_ubuntu_meta_releases(url)?
|
||||
.into_iter()
|
||||
.filter(|r| r.supported)
|
||||
.next_back()
|
||||
.map(|r| r.number)
|
||||
.ok_or_else(|| anyhow!("no supported Ubuntu release listed in {}", url))
|
||||
}
|
||||
|
||||
/// "24.04.1" names the same base release as "24.04": ubuntu-base
|
||||
/// publishes one tarball per YY.MM series.
|
||||
fn normalise_ubuntu_number(raw: &str) -> String {
|
||||
let mut parts = raw.splitn(3, '.');
|
||||
match (parts.next(), parts.next()) {
|
||||
(Some(a), Some(b)) => format!("{a}.{b}"),
|
||||
_ => raw.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A codename becomes its YY.MM number: from the static table, or —
|
||||
/// for a release younger than this build's table — from meta-release.
|
||||
fn ubuntu_codename_to_number(codename: &str) -> anyhow::Result<String> {
|
||||
let lower = codename.to_lowercase();
|
||||
if let Some((_, number)) = UBUNTU_CODENAMES.iter().find(|(name, _)| *name == lower) {
|
||||
return Ok((*number).to_string());
|
||||
}
|
||||
let releases = fetch_ubuntu_meta_releases("https://changelogs.ubuntu.com/meta-release")?;
|
||||
releases
|
||||
.iter()
|
||||
.find(|r| r.codename == lower)
|
||||
.map(|r| r.number.clone())
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Could not determine Ubuntu codename from {}",
|
||||
meta_release_url
|
||||
"Unknown Ubuntu version {codename:?}: use the YY.MM number (e.g. 26.04) \
|
||||
or a known codename (e.g. resolute, noble)"
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -362,82 +457,31 @@ pub fn resolve_distro_version(
|
||||
}?)
|
||||
}
|
||||
|
||||
/// Build a map of YY.MM version strings to codenames by parsing the Ubuntu
|
||||
/// meta-release file (e.g. "24.04" → "noble", "22.04" → "jammy").
|
||||
/// Uses the full meta-release (not -lts) so non-LTS versions are also covered.
|
||||
fn fetch_ubuntu_version_map() -> anyhow::Result<std::collections::HashMap<String, String>> {
|
||||
let client = crate::download::http_client(Some(Duration::from_secs(30)))?;
|
||||
let text = client
|
||||
.get("https://changelogs.ubuntu.com/meta-release")
|
||||
.send()
|
||||
.context("Failed to fetch Ubuntu meta-release")?
|
||||
.text()
|
||||
.context("Failed to read Ubuntu meta-release")?;
|
||||
|
||||
let mut map = std::collections::HashMap::new();
|
||||
let mut current_dist: Option<String> = None;
|
||||
|
||||
for line in text.lines() {
|
||||
if let Some(dist) = line.strip_prefix("Dist: ") {
|
||||
current_dist = Some(dist.trim().to_string());
|
||||
} else if let Some(version_str) = line.strip_prefix("Version: ") {
|
||||
// Version field may be "22.04", "22.04 LTS", or "24.04.1 LTS".
|
||||
// Normalise to YY.MM by taking the first two dot-separated components.
|
||||
let raw = version_str.split_whitespace().next().unwrap_or("");
|
||||
let normalised: String = {
|
||||
let mut parts = raw.splitn(3, '.');
|
||||
match (parts.next(), parts.next()) {
|
||||
(Some(a), Some(b)) => format!("{}.{}", a, b),
|
||||
_ => raw.to_string(),
|
||||
}
|
||||
};
|
||||
if let Some(dist) = ¤t_dist {
|
||||
map.insert(normalised, dist.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn resolve_ubuntu_version(version: Option<&str>) -> anyhow::Result<String> {
|
||||
let version = version.unwrap_or("latest");
|
||||
match version {
|
||||
"latest" => fetch_ubuntu_codename("https://changelogs.ubuntu.com/meta-release"),
|
||||
"latest" => fetch_ubuntu_latest_number("https://changelogs.ubuntu.com/meta-release"),
|
||||
"lts" | "latest-lts" => {
|
||||
fetch_ubuntu_codename("https://changelogs.ubuntu.com/meta-release-lts")
|
||||
fetch_ubuntu_latest_number("https://changelogs.ubuntu.com/meta-release-lts")
|
||||
}
|
||||
other => {
|
||||
// If it looks like a YY.MM version number, resolve it to a codename
|
||||
// via the meta-release file so the mapping never goes stale.
|
||||
if is_ubuntu_version_number(other) {
|
||||
let map = fetch_ubuntu_version_map()?;
|
||||
map.get(other).cloned().ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Unknown Ubuntu version '{}'. \
|
||||
Use the codename directly (e.g. noble, jammy) or check \
|
||||
https://changelogs.ubuntu.com/meta-release",
|
||||
other
|
||||
)
|
||||
})
|
||||
Ok(normalise_ubuntu_number(other))
|
||||
} else {
|
||||
// Treat as a codename and pass through (e.g. "noble", "jammy")
|
||||
Ok(other.to_string())
|
||||
ubuntu_codename_to_number(other)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true for strings of the form "YY.MM" (two numeric dot-separated components).
|
||||
/// Returns true for strings of the form "YY.MM" or "YY.MM.Z" — every
|
||||
/// dot-separated component numeric. Point releases normalise to YY.MM.
|
||||
fn is_ubuntu_version_number(s: &str) -> bool {
|
||||
let mut parts = s.splitn(3, '.');
|
||||
matches!(
|
||||
(parts.next(), parts.next(), parts.next()),
|
||||
(Some(a), Some(b), None)
|
||||
if !a.is_empty() && !b.is_empty()
|
||||
&& a.chars().all(|c| c.is_ascii_digit())
|
||||
&& b.chars().all(|c| c.is_ascii_digit())
|
||||
)
|
||||
let parts: Vec<&str> = s.split('.').collect();
|
||||
(2..=3).contains(&parts.len())
|
||||
&& parts
|
||||
.iter()
|
||||
.all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
|
||||
}
|
||||
|
||||
fn resolve_alpine_version(version: Option<&str>, arch: &str) -> anyhow::Result<String> {
|
||||
@@ -463,11 +507,10 @@ fn resolve_alpine_version(version: Option<&str>, arch: &str) -> anyhow::Result<S
|
||||
}
|
||||
|
||||
fn resolve_ubuntu_url(version: Option<&str>, arch: &str) -> anyhow::Result<String> {
|
||||
let codename = resolve_ubuntu_version(version)?;
|
||||
let number = resolve_ubuntu_version(version)?;
|
||||
let arch = map_arch(Distro::Ubuntu, arch);
|
||||
Ok(format!(
|
||||
"https://cdimage.ubuntu.com/ubuntu-base/{}/daily/current/{}-base-{}.tar.gz",
|
||||
codename, codename, arch
|
||||
"https://cdimage.ubuntu.com/ubuntu-base/releases/{number}/release/ubuntu-base-{number}-base-{arch}.tar.gz"
|
||||
))
|
||||
}
|
||||
|
||||
@@ -522,10 +565,12 @@ mod tests {
|
||||
assert_eq!(
|
||||
checksum_artifact_url(
|
||||
&Distro::Ubuntu,
|
||||
"https://cdimage.ubuntu.com/ubuntu-base/noble/daily/current/noble-base-amd64.tar.gz"
|
||||
"https://cdimage.ubuntu.com/ubuntu-base/releases/26.04/release/ubuntu-base-26.04-base-amd64.tar.gz"
|
||||
)
|
||||
.as_deref(),
|
||||
Some("https://cdimage.ubuntu.com/ubuntu-base/noble/daily/current/SHA256SUMS")
|
||||
Some(
|
||||
"https://cdimage.ubuntu.com/ubuntu-base/releases/26.04/release/SHA256SUMS"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -701,4 +746,41 @@ mod tests {
|
||||
oci("docker.io", "mydir/image", "latest")
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn meta_release_parsing_extracts_codename_number_pairs() {
|
||||
let text = "Dist: noble\nVersion: 24.04 LTS\nSupported: 1\n\
|
||||
Dist: questing\nVersion: 25.10\nSupported: 1\n\
|
||||
Dist: plucky\nVersion: 25.04\nSupported: 0\n";
|
||||
let releases = parse_ubuntu_meta_release(text);
|
||||
assert_eq!(releases.len(), 3);
|
||||
assert_eq!(releases[0].codename, "noble");
|
||||
assert_eq!(releases[0].number, "24.04");
|
||||
assert!(releases[0].supported);
|
||||
// A point release normalises to YY.MM
|
||||
let text = "Dist: noble\nVersion: 24.04.2 LTS\nSupported: 1\n";
|
||||
assert_eq!(parse_ubuntu_meta_release(text)[0].number, "24.04");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ubuntu_numbers_and_codenames_resolve_offline_and_identically() {
|
||||
// The alias promise: number and codename are the same image —
|
||||
// same resolved version, same URL, same cache key. No network.
|
||||
assert_eq!(
|
||||
resolve_ubuntu_version(Some("26.04")).unwrap(),
|
||||
resolve_ubuntu_version(Some("resolute")).unwrap()
|
||||
);
|
||||
assert_eq!(resolve_ubuntu_version(Some("resolute")).unwrap(), "26.04");
|
||||
assert_eq!(resolve_ubuntu_version(Some("NOBLE")).unwrap(), "24.04");
|
||||
// Point releases normalise to the series.
|
||||
assert_eq!(resolve_ubuntu_version(Some("24.04.2")).unwrap(), "24.04");
|
||||
let url = resolve_ubuntu_url(Some("resolute"), "amd64").unwrap();
|
||||
assert_eq!(
|
||||
url,
|
||||
"https://cdimage.ubuntu.com/ubuntu-base/releases/26.04/release/ubuntu-base-26.04-base-amd64.tar.gz"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_ubuntu_url(Some("26.04"), "amd64").unwrap(),
|
||||
resolve_ubuntu_url(Some("resolute"), "amd64").unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -624,7 +624,12 @@ fn generate_cache_filename(source: &ImageSource, arch: &str) -> String {
|
||||
Distro::Alpine => "alpine",
|
||||
};
|
||||
let distro_arch = map_arch(*distro, arch);
|
||||
// Get extension from URL
|
||||
// The extension comes from the URL. `version` is the
|
||||
// resolved value (resolve_source ran first), so this
|
||||
// derivation is offline: numbers and patch-resolved
|
||||
// strings pass through without lookups. A resolution
|
||||
// failure here degrades to the plain .tar.gz name rather
|
||||
// than blocking every cache-path computation.
|
||||
let url = resolve_distro_url(distro, version.as_deref(), arch).unwrap_or_default();
|
||||
let ext = get_tarball_extension(&url);
|
||||
format!(
|
||||
@@ -723,11 +728,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cache_filename_direct_tarball() {
|
||||
// Explicit codename/version: no network lookups
|
||||
// Explicit version, already resolved to a number: no network lookups
|
||||
let cache = RootfsCache::new("/cache");
|
||||
assert_eq!(
|
||||
cache.tarball_path(&direct_tarball(Distro::Ubuntu, "noble"), "amd64"),
|
||||
PathBuf::from("/cache/ubuntu-noble-amd64.tar.gz")
|
||||
cache.tarball_path(&direct_tarball(Distro::Ubuntu, "24.04"), "amd64"),
|
||||
PathBuf::from("/cache/ubuntu-24.04-amd64.tar.gz")
|
||||
);
|
||||
assert_eq!(
|
||||
cache.tarball_path(&direct_tarball(Distro::Alpine, "3.23.1"), "amd64"),
|
||||
|
||||
Reference in new issue
Block a user