From 38619c704b2128fb3747b9c10c699637c3dd1560 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 76b63389a..f326612ef 100644 --- a/Justfile +++ b/Justfile @@ -141,7 +141,7 @@ test-composefs bootloader filesystem boot_type seal_state *ARGS: --seal-state={{seal_state}} \ --boot-type={{boot_type}} \ {{ARGS}} \ - $(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 upgrade test: boot VM from published base image (with tmt deps added), # upgrade to locally-built image, reboot, then run readonly tests to verify. @@ -362,7 +362,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 \ + . # Build the upgrade source image: base image + tmt dependencies (rsync, nu, cloud-init) _build-upgrade-source-image: diff --git a/tmt/tests/Dockerfile.upgrade b/tmt/tests/Dockerfile.upgrade index a9e36ba50..561e2e0a7 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 | 8 ++ tmt/tests/booted/test-composefs-upgrade.nu | 111 +++++++++++++++++++++ tmt/tests/tests.fmf | 5 + 3 files changed, 124 insertions(+) create mode 100644 tmt/tests/booted/test-composefs-upgrade.nu diff --git a/tmt/plans/integration.fmf b/tmt/plans/integration.fmf index 46b14b0fa..5c4fa4eab 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -223,4 +223,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..5fcc633df --- /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 044376fae..c479f117a 100644 --- a/tmt/tests/tests.fmf +++ b/tmt/tests/tests.fmf @@ -126,3 +126,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