From b3a1c66d2abf1c68f1ecf500a5de6df6515b7f5e Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 2 Jul 2026 19:26:45 -0500 Subject: [PATCH] save generated system version in a file in all zones --- src/package.rs | 22 ++++++++++++++++++++++ tests/mod.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/package.rs b/src/package.rs index 5ac52ca..ff196bb 100644 --- a/src/package.rs +++ b/src/package.rs @@ -305,6 +305,28 @@ impl Package { self.get_output_path(name, output_directory), ))); + // Also record the stamped version at a fixed path *inside* the + // zone filesystem. The version otherwise lives only in the + // top-level `oxide.json` metadata, which is readable from the + // global zone (by opening the image tarball) but not from a + // process running inside the zone. Writing it here lets such a + // process read its own version (e.g. Nexus, for `/v1/version`). + // + // This mirrors the top-level `VERSION` file that `Tarball` + // outputs already carry. The parent directories must be added + // before the file, as zone images require parents before their + // children. + let system_version_path = Utf8Path::new("/var/oxide/system-version"); + inputs.0.extend( + zone_get_all_parent_inputs(system_version_path.parent().unwrap())? + .into_iter() + .map(BuildInput::AddDirectory), + ); + inputs.0.push(BuildInput::AddInMemoryFile { + dst_path: zone_archive_path(system_version_path)?, + contents: version.to_string(), + }); + // Add the package to "itself", but as a stamped version. // // We jump through some hoops to avoid modifying the archive diff --git a/tests/mod.rs b/tests/mod.rs index 6453a57..f847bc5 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -136,6 +136,49 @@ mod test { assert!(ents.next().is_none()); } + // Stamping a zone package records the version at a fixed path inside the + // zone filesystem (in addition to the top-level `oxide.json`), so a + // process running in the zone can read its own version. + #[tokio::test(flavor = "multi_thread")] + async fn test_stamp_zone_writes_system_version() { + let cfg = config::parse("tests/service-b/cfg.toml").unwrap(); + let package = cfg.packages.get(&MY_SERVICE_PACKAGE).unwrap(); + + let out = camino_tempfile::tempdir().unwrap(); + let build_config = BuildConfig::default(); + package + .create(&MY_SERVICE_PACKAGE, out.path(), &build_config) + .await + .unwrap(); + + let expected_semver = semver::Version::new(4, 5, 6); + let path = package + .stamp(&MY_SERVICE_PACKAGE, out.path(), &expected_semver) + .await + .unwrap(); + assert!(path.exists()); + + let gzr = flate2::read::GzDecoder::new(File::open(path).unwrap()); + let mut archive = Archive::new(gzr); + let mut ents = archive.entries().unwrap(); + + // The OMICRON1(5) format requires oxide.json to be the first entry. + assert_eq!("oxide.json", ents.next_path()); + + // Somewhere later, the in-zone version file should appear with the + // stamped version as its contents. + let mut found = None; + for entry in ents { + let mut entry = entry.unwrap(); + if entry_path(&entry) == "root/var/oxide/system-version" { + let mut s = String::new(); + entry.read_to_string(&mut s).unwrap(); + found = Some(s); + } + } + assert_eq!(found.as_deref(), Some(expected_semver.to_string().as_str())); + } + // Tests a rust package being placed into a non-Zone image. // // This is used for building packages that exist in the Global Zone,