Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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')]
Expand Down Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion tmt/plans/integration.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
65 changes: 62 additions & 3 deletions tmt/tests/Dockerfile.upgrade
Original file line number Diff line number Diff line change
@@ -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 <<EORUN
set -xeuo pipefail

allow_missing_verity=false
if [[ $filesystem == "xfs" ]]; then
allow_missing_verity=true
fi

if test "${boot_type}" = "uki"; then
/run/packaging/seal-uki /run/target /out /run/secrets $allow_missing_verity $seal_state
fi
EORUN

# Final stage: the upgrade image, optionally with a re-sealed UKI
FROM upgrade-base
ARG boot_type
RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \
--mount=type=bind,from=packaging,src=/,target=/run/packaging \
--mount=type=bind,from=sealed-upgrade-uki,src=/,target=/run/sealed-uki <<EORUN
set -xeuo pipefail
if test "${boot_type}" = "uki"; then
/run/packaging/finalize-uki /run/sealed-uki/out
fi
EORUN
111 changes: 111 additions & 0 deletions tmt/tests/booted/test-composefs-upgrade.nu
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# number: 39
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we not use the test-image-upgrade test here? Since it already has

    if ($imgsrc | str ends-with "-local") {
        bootc image copy-to-storage

        # A simple derived container that adds a file
        "FROM localhost/bootc
RUN touch /usr/share/testing-bootc-upgrade-apply
" | save Dockerfile
         # Build it
        podman build -t $imgsrc .
    }

It won't rebuild if we have the upgrade image built already

I think for now this is really good to have test, but imo we should have the same test (code) for all cases, so we don't end up missing something while special casing certain tests

# 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)"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The triple parentheses are redundant.

}

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)" } },
}
}
9 changes: 7 additions & 2 deletions tmt/tests/tests.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Loading