Skip to content

Commit c6112d2

Browse files
jbtrystramjmarrero
authored andcommitted
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. We skip the test for composefs (no aleph) and for upgrade tests since the original alpeh is written by an older bootc version. See [1] Ref #2038 [1] #2043 (comment) Assisted-by: OpenCode(Opus 4.6) Signed-off-by: jbtrystram <jbtrystram@redhat.com>
1 parent adab93e commit c6112d2

4 files changed

Lines changed: 115 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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
# In upgrade scenarios, the aleph file was written by the pre-upgrade bootc
16+
# which may not have the fields we're testing here (e.g. digest, target-image, labels).
17+
let is_upgrade = ($env.BOOTC_test_upgrade_image? | default "" | is-not-empty)
18+
if $is_upgrade {
19+
print "# Skipping aleph test in upgrade scenario (aleph written by older bootc)"
20+
tap ok
21+
exit 0
22+
}
23+
24+
# Detect composefs by checking if composefs field is present
25+
let is_composefs = (tap is_composefs)
26+
if $is_composefs {
27+
print "# TODO composefs: skipping test - No aleph file in composefs path"
28+
} else {
29+
30+
let aleph_path = "/sysroot/.bootc-aleph.json"
31+
let aleph = open $aleph_path
32+
33+
# Verify required fields exist and are non-empty
34+
assert ($aleph.kernel | is-not-empty) "kernel field should be non-empty"
35+
assert ($aleph.selinux | is-not-empty) "selinux field should be non-empty"
36+
37+
# Cross-check aleph fields against the booted image from bootc status
38+
let st = bootc status --json | from json
39+
let booted = $st.status.booted
40+
41+
# Verify the digest field matches the booted image digest
42+
assert ($aleph.digest | is-not-empty) "digest field should be non-empty"
43+
let booted_digest = $booted.image.imageDigest
44+
assert equal $aleph.digest $booted_digest "digest should match the booted image digest"
45+
46+
# Verify the target-image field matches the booted image reference
47+
let target_image = $aleph | get "target-image"
48+
assert ($target_image | is-not-empty) "target-image field should be non-empty"
49+
let booted_imgref = $booted.image.image.image
50+
assert equal $target_image $booted_imgref "target-image should match the booted image reference"
51+
52+
# The image field is optional (skipped when source is a /tmp path),
53+
# but if present it should be non-empty.
54+
let image = $aleph.image? | default null
55+
if $image != null {
56+
assert ($image | is-not-empty) "image field, if present, should be non-empty"
57+
let booted_imgref = $booted.image.image.image
58+
# The booted imgref contain the full digested pullspec
59+
# so we only check the beginning of the string
60+
assert ($image | str starts-with $booted_imgref) "image should match the booted image reference"
61+
62+
}
63+
64+
# The labels field may be absent if empty (skip_serializing_if), but if
65+
# present it should be a record and contain the bootc marker label.
66+
let labels = $aleph.labels? | default null
67+
if $labels != null {
68+
# Verify labels is a record (table-like key-value structure)
69+
assert (($labels | describe) =~ "record") "labels should be a record"
70+
# A bootc image should always carry the containers.bootc label
71+
let bootc_label = $labels | get "containers.bootc"
72+
assert ($bootc_label | is-not-empty) "containers.bootc label should be present"
73+
}
74+
}
75+
76+
tap ok

0 commit comments

Comments
 (0)