From 90783704e748b3b8dd849731d7c825929efa1139 Mon Sep 17 00:00:00 2001 From: jbtrystram Date: Wed, 4 Mar 2026 20:28:44 +0100 Subject: [PATCH] install/aleph: include the image labels in aleph Include the container labels in the aleph file, since they often contain useful information about the image provenance, such as the source commit the image was build from. Also we skip serializing the source image reference if it start with `/tmp` since this is a good signal it was source from a local copy of an image, e.g. in an osbuild environnement. Whith this, a build of Fedora CoreOS through osbuild goes from: ``` { "image": "/tmp/tmpb29j6pi3/image", "kernel": "6.18.12-200.fc43.x86_64", "selinux": "disabled", "timestamp": null, "version": "43.20260301.20.dev1" } ``` to ``` { "digest": "sha256:07bf537cc4e4d208eb0b978f76e5046e55529ce6192b982d8c1a41fa1d61b95a", "kernel": "6.18.13-200.fc43.x86_64", "labels": { "com.coreos.inputhash": "fe9883169714c593d98058606e886b9747710ed15ab1b9cdbd7fa538fb435b3c", "com.coreos.osname": "fedora-coreos", "com.coreos.stream": "testing-devel", "containers.bootc": "1", "io.buildah.version": "1.42.2", "org.opencontainers.image.description": "Fedora CoreOS testing-devel", "org.opencontainers.image.revision": "233fe18749c7d2749581e4307c4cac60967acde4", "org.opencontainers.image.source": "git@github.com:jbtrystram/fedora-coreos-config.git", "org.opencontainers.image.title": "Fedora CoreOS testing-devel", "org.opencontainers.image.version": "43.20260301.20.dev1", "ostree.bootable": "1", "ostree.commit": "89635f7cba9de932fc60d71a6bded65ad0db06a35c9d016da03ca7ade9ba4736", "ostree.final-diffid": "sha256:12787d84fa137cd5649a9005efe98ec9d05ea46245fdc50aecb7dd007f2035b1" }, "selinux": "disabled", "target-image": "ostree-image-signed:docker://quay.io/fedora/fedora-coreos:testing-devel", "timestamp": null, "version": "43.20260301.20.dev1" } ``` which is way more useful. See https://github.com/bootc-dev/bootc/issues/2038 Assisted-by: OpenCode(Opus 4.6) Signed-off-by: jbtrystram --- crates/lib/src/install.rs | 7 ++- crates/lib/src/install/aleph.rs | 35 +++++++++++++-- docs/src/bootc-install.md | 2 + tmt/tests/booted/readonly/013-test-aleph.nu | 49 +++++++++++++++++++++ 4 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 tmt/tests/booted/readonly/013-test-aleph.nu diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 6ab5a3cc0..19a49f17d 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -1209,7 +1209,12 @@ async fn install_container( osconfig::inject_root_ssh_authorized_keys(&root, sepolicy, contents)?; } - let aleph = InstallAleph::new(&src_imageref, &imgstate, &state.selinux_state)?; + let aleph = InstallAleph::new( + &src_imageref, + &state.target_imgref, + &imgstate, + &state.selinux_state, + )?; Ok((deployment, aleph)) } diff --git a/crates/lib/src/install/aleph.rs b/crates/lib/src/install/aleph.rs index 5983513da..d126905db 100644 --- a/crates/lib/src/install/aleph.rs +++ b/crates/lib/src/install/aleph.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use anyhow::{Context as _, Result}; use canon_json::CanonJsonSerialize as _; use cap_std_ext::{cap_std::fs::Dir, dirext::CapStdExtDirExt as _}; @@ -15,9 +17,19 @@ pub(crate) const BOOTC_ALEPH_PATH: &str = ".bootc-aleph.json"; /// be used to trace things like the specific version of `mkfs.ext4` or /// kernel version that was used. #[derive(Debug, Serialize)] +#[serde(rename_all = "kebab-case")] pub(crate) struct InstallAleph { /// Digested pull spec for installed image - pub(crate) image: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) image: Option, + /// The manifest digest of the installed image + pub(crate) digest: String, + /// The target image reference, used for subsequent updates + #[serde(rename = "target-image")] + pub(crate) target_image: String, + /// The OCI image labels from the installed image + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + pub(crate) labels: BTreeMap, /// The version number pub(crate) version: Option, /// The timestamp @@ -32,19 +44,34 @@ impl InstallAleph { #[context("Creating aleph data")] pub(crate) fn new( src_imageref: &ostree_container::OstreeImageReference, + target_imgref: &ostree_container::OstreeImageReference, imgstate: &ostree_container::store::LayeredImageState, selinux_state: &SELinuxFinalState, ) -> Result { let uname = rustix::system::uname(); - let labels = crate::status::labels_of_config(&imgstate.configuration); - let timestamp = labels + let oci_labels = crate::status::labels_of_config(&imgstate.configuration); + let timestamp = oci_labels .and_then(|l| { l.get(oci_spec::image::ANNOTATION_CREATED) .map(|s| s.as_str()) }) .and_then(bootc_utils::try_deserialize_timestamp); + let labels: BTreeMap = oci_labels + .map(|l| l.iter().map(|(k, v)| (k.clone(), v.clone())).collect()) + .unwrap_or_default(); + // When installing via osbuild, the source image is usually a + // temporary local container storage path (e.g. `/tmp/...`) which is not useful. + let image = if src_imageref.imgref.name.starts_with("/tmp") { + tracing::debug!("Not serializing the source imageref as it's a local temporary image."); + None + } else { + Some(src_imageref.imgref.name.clone()) + }; let r = InstallAleph { - image: src_imageref.imgref.name.clone(), + image, + target_image: target_imgref.imgref.name.clone(), + digest: imgstate.manifest_digest.to_string(), + labels, version: imgstate.version().as_ref().map(|s| s.to_string()), timestamp, kernel: uname.release().to_str()?.to_string(), diff --git a/docs/src/bootc-install.md b/docs/src/bootc-install.md index 87e709e7b..f5ccb1001 100644 --- a/docs/src/bootc-install.md +++ b/docs/src/bootc-install.md @@ -535,6 +535,8 @@ After installation, bootc writes a JSON file at the root of the physical filesystem (`.bootc-aleph.json`) containing installation provenance information: - The source image reference and digest +- The target image reference (if provided) +- The OCI image labels from the installed image - Installation timestamp - bootc version - Kernel version diff --git a/tmt/tests/booted/readonly/013-test-aleph.nu b/tmt/tests/booted/readonly/013-test-aleph.nu new file mode 100644 index 000000000..f7b0a76cd --- /dev/null +++ b/tmt/tests/booted/readonly/013-test-aleph.nu @@ -0,0 +1,49 @@ +use std assert +use tap.nu + +tap begin "verify bootc aleph file contents" + +let aleph_path = "/sysroot/.bootc-aleph.json" +let aleph = open $aleph_path + +# 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" + +# Cross-check aleph fields against the booted image from bootc status +let st = bootc status --json | from json +let booted = $st.status.booted + +# Verify the digest field matches the booted image digest +assert ($aleph.digest | is-not-empty) "digest field should be non-empty" +let booted_digest = $booted.image.imageDigest +assert equal $aleph.digest $booted_digest "digest should match the booted image digest" + +# Verify the target-image field matches the booted image reference +let target_image = $aleph | get "target-image" +assert ($target_image | is-not-empty) "target-image field should be non-empty" +let booted_imgref = $booted.image.image.image +assert equal $target_image $booted_imgref "target-image should match the booted image reference" + +# The image field is optional (skipped when source is a /tmp path), +# but if present it should be non-empty. +let image = $aleph.image? | default null +if $image != null { + assert ($image | is-not-empty) "image field, if present, should be non-empty" + let booted_imgref = $booted.image.image.image + assert equal $image $booted_imgref "target-image should match the booted image reference" + +} + +# The labels field may be absent if empty (skip_serializing_if), but if +# present it should be a record and contain the bootc marker label. +let labels = $aleph.labels? | default null +if $labels != null { + # Verify labels is a record (table-like key-value structure) + assert (($labels | describe) =~ "record") "labels should be a record" + # A bootc image should always carry the containers.bootc label + let bootc_label = $labels | get "containers.bootc" + assert ($bootc_label | is-not-empty) "containers.bootc label should be present" +} + +tap ok