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/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/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 < 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