Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions crates/lib/src/install/aleph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ use ostree_ext::{container as ostree_container, oci_spec};
use serde::Serialize;

use super::SELinuxFinalState;
use crate::utils::symlink_idempotent;

/// Path to initially deployed version information
pub(crate) const BOOTC_ALEPH_PATH: &str = ".bootc-aleph.json";
/// Compatibility symlink target.
/// Legacy from coreOS and osbuild where the
/// aleph data is accessible at this expected path.
/// See <https://github.com/coreos/fedora-coreos-tracker/issues/2187>
const ALEPH_SYMLINK: &str = ".aleph-version.json";

/// The "aleph" version information is injected into /root/.bootc-aleph.json
/// and contains the image ID that was initially used to install. This can
Expand Down Expand Up @@ -80,11 +86,16 @@ impl InstallAleph {
Ok(r)
}

/// Serialize to a file in the target root.
/// Serialize to a file in the target root, and create a compatibility symlink.
pub(crate) fn write_to(&self, root: &Dir) -> Result<()> {
root.atomic_replace_with(BOOTC_ALEPH_PATH, |f| {
anyhow::Ok(self.to_canon_json_writer(f)?)
})
.context("Writing aleph version")
.context("Writing aleph version")?;
// Create a symlink so the aleph data is also accessible at the
// legacy path.
symlink_idempotent(root, BOOTC_ALEPH_PATH, ALEPH_SYMLINK)
.context("Creating aleph symlink")?;
Ok(())
}
}
42 changes: 42 additions & 0 deletions crates/lib/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,29 @@ pub fn have_executable(name: &str) -> Result<bool> {
Ok(false)
}

/// Idempotently ensure a symlink `link` -> `target` exists in `dir`.
///
/// If the symlink already exists and points to the expected target, this is a
/// no-op. If it exists but points elsewhere, it is replaced.
pub(crate) fn symlink_idempotent(dir: &Dir, target: &str, link: &str) -> Result<()> {
match dir.symlink(target, link) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
let existing = dir
.read_link(link)
.with_context(|| format!("Reading existing symlink {link}"))?;
if existing == std::path::Path::new(target) {
return Ok(());
}
dir.remove_file(link)
.with_context(|| format!("Removing stale symlink {link}"))?;
dir.symlink(target, link)
.with_context(|| format!("Creating symlink {link} -> {target}"))
}
Err(e) => Err(e).with_context(|| format!("Creating symlink {link} -> {target}")),
}
}

/// Given a target directory, if it's a read-only mount, then remount it writable
#[context("Opening {target} with writable mount")]
pub(crate) fn open_dir_remount_rw(root: &Dir, target: &Utf8Path) -> Result<Dir> {
Expand Down Expand Up @@ -336,4 +359,23 @@ mod tests {
assert!(have_executable("true").unwrap());
assert!(!have_executable("someexethatdoesnotexist").unwrap());
}

#[test]
fn test_symlink_idempotent() {
let td = cap_std_ext::cap_tempfile::TempDir::new(
cap_std_ext::cap_std::ambient_authority(),
)
.unwrap();
// First call creates the symlink
symlink_idempotent(&td, "target-a", "link").unwrap();
assert_eq!(td.read_link("link").unwrap(), Path::new("target-a"));

// Second call with the same target is a no-op
symlink_idempotent(&td, "target-a", "link").unwrap();
assert_eq!(td.read_link("link").unwrap(), Path::new("target-a"));

// Call with a different target replaces the symlink
symlink_idempotent(&td, "target-b", "link").unwrap();
assert_eq!(td.read_link("link").unwrap(), Path::new("target-b"));
}
}
2 changes: 2 additions & 0 deletions docs/src/bootc-install.md
Original file line number Diff line number Diff line change
Expand Up @@ -544,3 +544,5 @@ filesystem (`.bootc-aleph.json`) containing installation provenance information:

This file is useful for auditing and understanding how a system was provisioned.
From the booted system, this file is accessible at `/sysroot/.bootc-aleph.json`.
A compatibility symlink `.aleph-version.json` is also created, pointing to
`.bootc-aleph.json`.
6 changes: 6 additions & 0 deletions tmt/tests/booted/readonly/013-test-aleph.nu
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ if $is_composefs {
} else {

let aleph_path = "/sysroot/.bootc-aleph.json"
let symlink_path = "/sysroot/.aleph-version.json"
let aleph = open $aleph_path

# Verify that the compatibility symlink exists and points to the aleph file
assert (($symlink_path | path exists)) "aleph symlink should exist"
let aleph_via_symlink = open $symlink_path
assert equal $aleph $aleph_via_symlink "symlink content should match aleph file"

# Verify required fields exist and are non-empty
assert ($aleph.kernel | is-not-empty) "kernel field should be non-empty"
assert ($aleph.selinux | is-not-empty) "selinux field should be non-empty"
Expand Down
Loading