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
+177
-90
No files matched your search
+168
-86
@@ -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.
|
/// Ubuntu releases with base images: `(codename, YY.MM)`, oldest
|
||||||
/// Pass the LTS-only URL to get the latest LTS, or the full URL for the latest release.
|
/// first. Static on purpose — `ubuntu:26.04` and `ubuntu:resolute`
|
||||||
fn fetch_ubuntu_codename(meta_release_url: &str) -> anyhow::Result<String> {
|
/// must resolve identically and offline, and a codename never changes
|
||||||
let client = crate::download::http_client(Some(Duration::from_secs(30)))?;
|
/// meaning. A newer release than this table needs an entry only for
|
||||||
let text = client
|
/// its codename; its number always resolves without one.
|
||||||
.get(meta_release_url)
|
const UBUNTU_CODENAMES: &[(&str, &str)] = &[
|
||||||
.send()
|
("xenial", "16.04"),
|
||||||
.with_context(|| format!("Failed to fetch {}", meta_release_url))?
|
("bionic", "18.04"),
|
||||||
.text()
|
("cosmic", "18.10"),
|
||||||
.with_context(|| format!("Failed to read {}", meta_release_url))?;
|
("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;
|
/// One entry of a meta-release file.
|
||||||
let mut latest: Option<String> = None;
|
#[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() {
|
for line in text.lines() {
|
||||||
if let Some(dist) = line.strip_prefix("Dist: ") {
|
if let Some(dist) = line.strip_prefix("Dist: ") {
|
||||||
current_dist = Some(dist.trim().to_string());
|
flush(&mut current, &mut releases);
|
||||||
} else if line.trim_start().starts_with("Supported: 1") {
|
current = Some((dist.trim().to_string(), String::new(), false));
|
||||||
if let Some(dist) = current_dist.take() {
|
} else if let Some(version) = line.strip_prefix("Version: ") {
|
||||||
latest = Some(dist);
|
// "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((_, _, supported)) = current.as_mut() {
|
||||||
|
*supported = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flush(&mut current, &mut releases);
|
||||||
|
releases
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
latest.ok_or_else(|| {
|
/// 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!(
|
anyhow!(
|
||||||
"Could not determine Ubuntu codename from {}",
|
"Unknown Ubuntu version {codename:?}: use the YY.MM number (e.g. 26.04) \
|
||||||
meta_release_url
|
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> {
|
fn resolve_ubuntu_version(version: Option<&str>) -> anyhow::Result<String> {
|
||||||
let version = version.unwrap_or("latest");
|
let version = version.unwrap_or("latest");
|
||||||
match version {
|
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" => {
|
"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 => {
|
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) {
|
if is_ubuntu_version_number(other) {
|
||||||
let map = fetch_ubuntu_version_map()?;
|
Ok(normalise_ubuntu_number(other))
|
||||||
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
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
// Treat as a codename and pass through (e.g. "noble", "jammy")
|
ubuntu_codename_to_number(other)
|
||||||
Ok(other.to_string())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 {
|
fn is_ubuntu_version_number(s: &str) -> bool {
|
||||||
let mut parts = s.splitn(3, '.');
|
let parts: Vec<&str> = s.split('.').collect();
|
||||||
matches!(
|
(2..=3).contains(&parts.len())
|
||||||
(parts.next(), parts.next(), parts.next()),
|
&& parts
|
||||||
(Some(a), Some(b), None)
|
.iter()
|
||||||
if !a.is_empty() && !b.is_empty()
|
.all(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()))
|
||||||
&& a.chars().all(|c| c.is_ascii_digit())
|
|
||||||
&& b.chars().all(|c| c.is_ascii_digit())
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_alpine_version(version: Option<&str>, arch: &str) -> anyhow::Result<String> {
|
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> {
|
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);
|
let arch = map_arch(Distro::Ubuntu, arch);
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
"https://cdimage.ubuntu.com/ubuntu-base/{}/daily/current/{}-base-{}.tar.gz",
|
"https://cdimage.ubuntu.com/ubuntu-base/releases/{number}/release/ubuntu-base-{number}-base-{arch}.tar.gz"
|
||||||
codename, codename, arch
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,10 +565,12 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
checksum_artifact_url(
|
checksum_artifact_url(
|
||||||
&Distro::Ubuntu,
|
&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(),
|
.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")
|
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",
|
Distro::Alpine => "alpine",
|
||||||
};
|
};
|
||||||
let distro_arch = map_arch(*distro, arch);
|
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 url = resolve_distro_url(distro, version.as_deref(), arch).unwrap_or_default();
|
||||||
let ext = get_tarball_extension(&url);
|
let ext = get_tarball_extension(&url);
|
||||||
format!(
|
format!(
|
||||||
@@ -723,11 +728,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cache_filename_direct_tarball() {
|
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");
|
let cache = RootfsCache::new("/cache");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cache.tarball_path(&direct_tarball(Distro::Ubuntu, "noble"), "amd64"),
|
cache.tarball_path(&direct_tarball(Distro::Ubuntu, "24.04"), "amd64"),
|
||||||
PathBuf::from("/cache/ubuntu-noble-amd64.tar.gz")
|
PathBuf::from("/cache/ubuntu-24.04-amd64.tar.gz")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cache.tarball_path(&direct_tarball(Distro::Alpine, "3.23.1"), "amd64"),
|
cache.tarball_path(&direct_tarball(Distro::Alpine, "3.23.1"), "amd64"),
|
||||||
|
|||||||
Reference in new issue
Block a user