Skip to content

Commit a8a3648

Browse files
committed
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 #2038 Assisted-by: OpenCode(Opus 4.6) Signed-off-by: jbtrystram <jbtrystram@redhat.com>
1 parent adab93e commit a8a3648

4 files changed

Lines changed: 106 additions & 5 deletions

File tree

crates/lib/src/install.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1214,7 +1214,12 @@ async fn install_container(
12141214
osconfig::inject_root_ssh_authorized_keys(&root, sepolicy, contents)?;
12151215
}
12161216

1217-
let aleph = InstallAleph::new(&src_imageref, &imgstate, &state.selinux_state)?;
1217+
let aleph = InstallAleph::new(
1218+
&src_imageref,
1219+
&state.target_imgref,
1220+
&imgstate,
1221+
&state.selinux_state,
1222+
)?;
12181223
Ok((deployment, aleph))
12191224
}
12201225

crates/lib/src/install/aleph.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::BTreeMap;
2+
13
use anyhow::{Context as _, Result};
24
use canon_json::CanonJsonSerialize as _;
35
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";
1517
/// be used to trace things like the specific version of `mkfs.ext4` or
1618
/// kernel version that was used.
1719
#[derive(Debug, Serialize)]
20+
#[serde(rename_all = "kebab-case")]
1821
pub(crate) struct InstallAleph {
1922
/// Digested pull spec for installed image
20-
pub(crate) image: String,
23+
#[serde(skip_serializing_if = "Option::is_none")]
24+
pub(crate) image: Option<String>,
25+
/// The manifest digest of the installed image
26+
pub(crate) digest: String,
27+
/// The target image reference, used for subsequent updates
28+
#[serde(rename = "target-image")]
29+
pub(crate) target_image: String,
30+
/// The OCI image labels from the installed image
31+
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
32+
pub(crate) labels: BTreeMap<String, String>,
2133
/// The version number
2234
pub(crate) version: Option<String>,
2335
/// The timestamp
@@ -32,19 +44,34 @@ impl InstallAleph {
3244
#[context("Creating aleph data")]
3345
pub(crate) fn new(
3446
src_imageref: &ostree_container::OstreeImageReference,
47+
target_imgref: &ostree_container::OstreeImageReference,
3548
imgstate: &ostree_container::store::LayeredImageState,
3649
selinux_state: &SELinuxFinalState,
3750
) -> Result<Self> {
3851
let uname = rustix::system::uname();
39-
let labels = crate::status::labels_of_config(&imgstate.configuration);
40-
let timestamp = labels
52+
let oci_labels = crate::status::labels_of_config(&imgstate.configuration);
53+
let timestamp = oci_labels
4154
.and_then(|l| {
4255
l.get(oci_spec::image::ANNOTATION_CREATED)
4356
.map(|s| s.as_str())
4457
})
4558
.and_then(bootc_utils::try_deserialize_timestamp);
59+
let labels: BTreeMap<String, String> = oci_labels
60+
.map(|l| l.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
61+
.unwrap_or_default();
62+
// When installing via osbuild, the source image is usually a
63+
// temporary local container storage path (e.g. `/tmp/...`) which is not useful.
64+
let image = if src_imageref.imgref.name.starts_with("/tmp") {
65+
tracing::debug!("Not serializing the source imageref as it's a local temporary image.");
66+
None
67+
} else {
68+
Some(src_imageref.imgref.name.clone())
69+
};
4670
let r = InstallAleph {
47-
image: src_imageref.imgref.name.clone(),
71+
image,
72+
target_image: target_imgref.imgref.name.clone(),
73+
digest: imgstate.manifest_digest.to_string(),
74+
labels,
4875
version: imgstate.version().as_ref().map(|s| s.to_string()),
4976
timestamp,
5077
kernel: uname.release().to_str()?.to_string(),

docs/src/bootc-install.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,8 @@ After installation, bootc writes a JSON file at the root of the physical
535535
filesystem (`.bootc-aleph.json`) containing installation provenance information:
536536

537537
- The source image reference and digest
538+
- The target image reference (if provided)
539+
- The OCI image labels from the installed image
538540
- Installation timestamp
539541
- bootc version
540542
- Kernel version
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# number: 13
2+
# tmt:
3+
# summary: Test the aleph file exist and contains the correct info
4+
# extra:
5+
# fixme_skip_if_composefs: true
6+
#
7+
# Validates the alpeh file exist and contains the image digest
8+
# and the target-image reference in applicable cases.
9+
10+
use std assert
11+
use tap.nu
12+
13+
tap begin "verify bootc aleph file contents"
14+
15+
# Detect composefs by checking if composefs field is present
16+
let is_composefs = (tap is_composefs)
17+
if $is_composefs {
18+
print "# TODO composefs: skipping test - No aleph file in composefs path"
19+
} else {
20+
21+
let aleph_path = "/sysroot/.bootc-aleph.json"
22+
let aleph = open $aleph_path
23+
24+
# Verify required fields exist and are non-empty
25+
assert ($aleph.kernel | is-not-empty) "kernel field should be non-empty"
26+
assert ($aleph.selinux | is-not-empty) "selinux field should be non-empty"
27+
28+
# Cross-check aleph fields against the booted image from bootc status
29+
let st = bootc status --json | from json
30+
let booted = $st.status.booted
31+
32+
# Verify the digest field matches the booted image digest
33+
assert ($aleph.digest | is-not-empty) "digest field should be non-empty"
34+
let booted_digest = $booted.image.imageDigest
35+
assert equal $aleph.digest $booted_digest "digest should match the booted image digest"
36+
37+
# Verify the target-image field matches the booted image reference
38+
let target_image = $aleph | get "target-image"
39+
assert ($target_image | is-not-empty) "target-image field should be non-empty"
40+
let booted_imgref = $booted.image.image.image
41+
assert equal $target_image $booted_imgref "target-image should match the booted image reference"
42+
43+
# The image field is optional (skipped when source is a /tmp path),
44+
# but if present it should be non-empty.
45+
let image = $aleph.image? | default null
46+
if $image != null {
47+
assert ($image | is-not-empty) "image field, if present, should be non-empty"
48+
let booted_imgref = $booted.image.image.image
49+
# The booted imgref contain the full digested pullspec
50+
# so we only check the beginning of the string
51+
assert ($image | str starts-with $booted_imgref) "image should match the booted image reference"
52+
53+
}
54+
55+
# The labels field may be absent if empty (skip_serializing_if), but if
56+
# present it should be a record and contain the bootc marker label.
57+
let labels = $aleph.labels? | default null
58+
if $labels != null {
59+
# Verify labels is a record (table-like key-value structure)
60+
assert (($labels | describe) =~ "record") "labels should be a record"
61+
# A bootc image should always carry the containers.bootc label
62+
let bootc_label = $labels | get "containers.bootc"
63+
assert ($bootc_label | is-not-empty) "containers.bootc label should be present"
64+
}
65+
}
66+
67+
tap ok

0 commit comments

Comments
 (0)