From 5ff42d3e1bf9ab58c254b5d0d71df5a137a2b60c Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 6 Mar 2026 22:47:44 +0000 Subject: [PATCH 1/2] build-sys: Re-seal upgrade image for composefs UKI builds The upgrade test image (localhost/bootc-upgrade) was previously a simple one-layer addition on top of localhost/bootc that did not go through the sealing pipeline. This meant sealed composefs builds could not properly test upgrades, since the upgrade image lacked a signed UKI with the correct composefs digest. Rework Dockerfile.upgrade into a multi-stage build that mirrors the main Dockerfile sealing flow: when boot_type=uki, it computes the composefs digest of the upgrade rootfs, generates and optionally signs a UKI via seal-uki, and finalizes it with finalize-uki. For non-UKI builds, the extra stages are effectively no-ops and the image remains a simple derived layer. Update _build-upgrade-image in the Justfile to pass the required build arguments (boot_type, seal_state, filesystem) and build secrets (secureboot keys). Extra container capabilities (CAP_ALL, fuse device) are only added for UKI builds that need composefs support. Assisted-by: OpenCode (claude-opus-4) --- Justfile | 21 ++++++++++-- tmt/tests/Dockerfile.upgrade | 65 ++++++++++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/Justfile b/Justfile index a33e9640f..13d0eca12 100644 --- a/Justfile +++ b/Justfile @@ -138,7 +138,7 @@ test-composefs bootloader filesystem boot_type seal_state: --filesystem={{filesystem}} \ --seal-state={{seal_state}} \ --boot-type={{boot_type}} \ - $(if [ "{{boot_type}}" = "uki" ]; then echo "readonly"; else echo "integration"; fi) + $(if [ "{{boot_type}}" = "uki" ]; then echo "readonly composefs-upgrade"; else echo "integration"; fi) # Run cargo fmt and clippy checks in container [group('core')] @@ -314,7 +314,24 @@ _keygen: ./hack/generate-secureboot-keys _build-upgrade-image: - cat tmt/tests/Dockerfile.upgrade | podman build -t {{upgrade_img}} --from={{base_img}} - + #!/bin/bash + set -xeuo pipefail + # Secrets are always available (test-tmt depends on build which runs _keygen). + # Extra capabilities are only needed for UKI builds (composefs + fuse). + extra_args=() + if [ "{{boot_type}}" = "uki" ]; then + extra_args+=(--cap-add=all --security-opt=label=type:container_runtime_t --device /dev/fuse) + fi + podman build \ + --build-arg boot_type={{boot_type}} \ + --build-arg seal_state={{seal_state}} \ + --build-arg filesystem={{filesystem}} \ + --secret=id=secureboot_key,src=target/test-secureboot/db.key \ + --secret=id=secureboot_cert,src=target/test-secureboot/db.crt \ + "${extra_args[@]}" \ + -t {{upgrade_img}} \ + -f tmt/tests/Dockerfile.upgrade \ + . # Copy an image from user podman storage to root's podman storage # This allows building as regular user then running privileged tests diff --git a/tmt/tests/Dockerfile.upgrade b/tmt/tests/Dockerfile.upgrade index a9e36ba50..9019dcdfa 100644 --- a/tmt/tests/Dockerfile.upgrade +++ b/tmt/tests/Dockerfile.upgrade @@ -1,3 +1,62 @@ -# Just creates a file as a new layer for a synthetic upgrade test -FROM localhost/bootc -RUN touch --reference=/usr/bin/bash /usr/share/testing-bootc-upgrade-apply +# Creates a synthetic upgrade image for testing. +# For non-UKI builds, this just adds a marker file on top of localhost/bootc. +# For UKI builds (boot_type=uki), the image is re-sealed with a new composefs +# digest and (optionally signed) UKI. +# +# Build secrets required (for sealed builds): +# secureboot_key, secureboot_cert +ARG boot_type=bls +ARG seal_state=unsealed +ARG filesystem=ext4 + +# Capture contrib/packaging scripts for use in later stages +FROM scratch AS packaging +COPY contrib/packaging / + +# Create the upgrade content (a simple marker file). +# For UKI builds, we also remove the existing UKI so that seal-uki can +# regenerate it with the correct composefs digest for this derived image. +FROM localhost/bootc AS upgrade-base +ARG boot_type +RUN touch --reference=/usr/bin/bash /usr/share/testing-bootc-upgrade-apply && \ + if test "${boot_type}" = "uki"; then rm -rf /boot/EFI/Linux/*.efi; fi + +# Tools for sealing (only meaningfully used for UKI builds) +FROM localhost/bootc AS tools +RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ + --mount=type=bind,from=packaging,src=/,target=/run/packaging \ + /run/packaging/initialize-sealing-tools + +# Generate a sealed UKI for the upgrade image. +# bootc is already installed in localhost/bootc (our tools base); the +# container ukify command it provides is needed for seal-uki. +FROM tools AS sealed-upgrade-uki +ARG boot_type seal_state filesystem +RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ + --mount=type=secret,id=secureboot_key \ + --mount=type=secret,id=secureboot_cert \ + --mount=type=bind,from=packaging,src=/,target=/run/packaging \ + --mount=type=bind,from=upgrade-base,src=/,target=/run/target < Date: Fri, 6 Mar 2026 22:47:56 +0000 Subject: [PATCH 2/2] tests: Add composefs-upgrade test for sealed UKI builds The goal is ensuring we have upgrade coverage also for sealed UKIs; most of the other update code paths (because tmt doesn't make it easy to have a registry) do on-machine synthetic updates. Assisted-by: OpenCode (claude-opus-4) Signed-off-by: Colin Walters --- tmt/plans/integration.fmf | 17 +++- tmt/tests/booted/test-composefs-upgrade.nu | 111 +++++++++++++++++++++ tmt/tests/tests.fmf | 9 +- 3 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 tmt/tests/booted/test-composefs-upgrade.nu diff --git a/tmt/plans/integration.fmf b/tmt/plans/integration.fmf index ae6374690..a695fd9c8 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -182,8 +182,15 @@ execute: test: - /tmt/tests/tests/test-34-user-agent +/plan-35-upgrade-preflight-disk-check: + summary: Verify pre-flight disk space check rejects images with inflated layer sizes + discover: + how: fmf + test: + - /tmt/tests/tests/test-35-upgrade-preflight-disk-check + /plan-36-rollback: - summary: Test bootc rollback functionality through image switch and rollback cycle + summary: Test bootc rollback functionality discover: how: fmf test: @@ -202,4 +209,12 @@ execute: how: fmf test: - /tmt/tests/tests/test-38-install-bootloader-none + +/plan-39-composefs-upgrade: + summary: Test composefs upgrade with pre-built (optionally sealed) image + discover: + how: fmf + test: + - /tmt/tests/tests/test-39-composefs-upgrade + extra-try_bind_storage: true # END GENERATED PLANS diff --git a/tmt/tests/booted/test-composefs-upgrade.nu b/tmt/tests/booted/test-composefs-upgrade.nu new file mode 100644 index 000000000..edacd6b37 --- /dev/null +++ b/tmt/tests/booted/test-composefs-upgrade.nu @@ -0,0 +1,111 @@ +# number: 39 +# extra: +# try_bind_storage: true +# tmt: +# summary: Test composefs upgrade with pre-built (optionally sealed) image +# duration: 30m +# +# This test verifies that upgrading a composefs system works correctly, +# including sealed UKI images. The upgrade image is pre-built on the host +# with proper sealing and made available via bind-storage-ro. +# +use std assert +use tap.nu + +bootc status +journalctl --list-boots + +let st = bootc status --json | from json +let booted = $st.status.booted.image +let is_composefs = (tap is_composefs) + +# This test only makes sense for composefs +if not $is_composefs { + tap begin "composefs upgrade (skipped - not composefs)" + print "# SKIP: not running on composefs" + tap ok + exit 0 +} + +def upgrade_image [] { + $env.BOOTC_upgrade_image? | default "localhost/bootc-upgrade" +} + +# First boot: save the original verity digest, then switch to the upgrade image +def first_boot [] { + tap begin "composefs upgrade with pre-built image" + + # Save the original verity so we can check for two UKIs after upgrade + $st.status.booted.composefs.verity | save /var/original-verity + + let img = (upgrade_image) + print $"Switching to upgrade image: ($img)" + + # The upgrade image should be available via host container storage + # (passed through --bind-storage-ro by bcvk) + bootc switch --transport containers-storage $img + tmt-reboot +} + +# Second boot: verify the upgrade succeeded and both UKIs exist +def second_boot [] { + print "Verifying composefs upgrade" + + # Verify we booted from the upgrade image + let img = (upgrade_image) + assert equal $booted.image.transport containers-storage + assert equal $booted.image.image $img + + # Verify composefs is still active after the upgrade + assert (tap is_composefs) "composefs should still be active after upgrade" + + # Verify the upgrade marker file exists + assert ("/usr/share/testing-bootc-upgrade-apply" | path exists) "upgrade marker file should exist" + + # Verify composefs properties are preserved after the upgrade + let composefs_info = $st.status.booted.composefs + print $"composefs info: ($composefs_info)" + + # Verify there is a valid verity digest (composefs was properly deployed) + assert (($composefs_info.verity | str length) > 0) "composefs verity digest should be present" + + # For UKI boot type, verify both the original and upgrade UKIs exist on the ESP + if ($composefs_info.bootType | str downcase) == "uki" { + let bootloader = ($composefs_info.bootloader | str downcase) + + # UKIs are stored in EFI/Linux/bootc/ on the ESP + let boot_dir = if $bootloader == "systemd" { + mkdir /var/tmp/efi + mount /dev/vda2 /var/tmp/efi + "/var/tmp/efi/EFI/Linux/bootc" + } else { + "/sysroot/boot/EFI/Linux/bootc" + } + + let original_verity = (open /var/original-verity | str trim) + let upgrade_verity = $composefs_info.verity + + print $"boot_dir: ($boot_dir)" + print $"original verity: ($original_verity)" + print $"upgrade verity: ($upgrade_verity)" + + # The two verities must differ since the upgrade image has different content + assert ($original_verity != $upgrade_verity) "upgrade should produce a different verity digest" + + # There should be two .efi UKI files on the ESP: one for the booted + # deployment (upgrade) and one for the rollback (original) + let efi_files = (glob $"($boot_dir)/*.efi") + print $"EFI files: ($efi_files)" + assert ((($efi_files | length) >= 2)) $"expected at least 2 UKIs on ESP, found ($efi_files | length)" + } + + tap ok +} + +def main [] { + match $env.TMT_REBOOT_COUNT? { + null | "0" => first_boot, + "1" => second_boot, + $o => { error make { msg: $"Invalid TMT_REBOOT_COUNT ($o)" } }, + } +} diff --git a/tmt/tests/tests.fmf b/tmt/tests/tests.fmf index 851d6b293..d9a3e7de6 100644 --- a/tmt/tests/tests.fmf +++ b/tmt/tests/tests.fmf @@ -104,11 +104,11 @@ /test-35-upgrade-preflight-disk-check: summary: Verify pre-flight disk space check rejects images with inflated layer sizes - duration: 20m + duration: 10m test: nu booted/test-upgrade-preflight-disk-check.nu /test-36-rollback: - summary: Test bootc rollback functionality through image switch and rollback cycle + summary: Test bootc rollback functionality duration: 30m test: nu booted/test-rollback.nu @@ -121,3 +121,8 @@ summary: Test bootc install with --bootloader=none duration: 30m test: nu booted/test-install-bootloader-none.nu + +/test-39-composefs-upgrade: + summary: Test composefs upgrade with pre-built (optionally sealed) image + duration: 30m + test: nu booted/test-composefs-upgrade.nu