Skip to content

Commit 3cc8e40

Browse files
committed
Add an "upgrade from previous" test
We have tests that do upgrades, but they start from the *new* bootc. Add a `just`+CI workflow that starts from a stable shipped image (we just need to inject tmt deps + nu). The readonly test then gains a helper which optionally performs an upgrade to the new target. IOW the flow is - deploy stock image with unmodified bootc etc - upgrade - run readonly tests It could of course make sense to run *all* of the tests this way as an optional thing, nothing blocks that, but it would be *another* entry in our matrix and we're going to need to figure out how to wrangle that matrix size. Also do to that we'd need to abstract over TMT_REBOOT_COUNT. Assisted-by: OpenCode (Claude claude-opus-4-6) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 48124b0 commit 3cc8e40

8 files changed

Lines changed: 220 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,52 @@ jobs:
262262
${{ env.ARCH }}"
263263
path: /var/tmp/tmt
264264

265+
# Test the upgrade path: boot from published base image, upgrade to locally-built image,
266+
# then run readonly tests to verify the upgrade worked.
267+
# Excluded: centos-9 (lacks systemd.extra-unit.* support needed for --bind-storage-ro)
268+
test-upgrade:
269+
needs: package
270+
strategy:
271+
fail-fast: false
272+
matrix:
273+
test_os: [fedora-43, centos-10]
274+
variant: [ostree, composefs]
275+
276+
runs-on: ubuntu-24.04
277+
278+
steps:
279+
- uses: actions/checkout@v6
280+
- name: Bootc Ubuntu Setup
281+
uses: bootc-dev/actions/bootc-ubuntu-setup@main
282+
with:
283+
libvirt: true
284+
- name: Install tmt
285+
run: pip install --user "tmt[provision-virtual]"
286+
287+
- name: Setup env
288+
run: |
289+
BASE=$(just pullspec-for-os base ${{ matrix.test_os }})
290+
echo "BOOTC_base=${BASE}" >> $GITHUB_ENV
291+
echo "BOOTC_variant=${{ matrix.variant }}" >> $GITHUB_ENV
292+
echo "BOOTC_SKIP_PACKAGE=1" >> $GITHUB_ENV
293+
echo "RUST_BACKTRACE=full" >> $GITHUB_ENV
294+
295+
- name: Download package artifacts
296+
uses: actions/download-artifact@v8
297+
with:
298+
name: packages-${{ matrix.test_os }}
299+
path: target/packages/
300+
301+
- name: Run upgrade test
302+
run: just test-upgrade
303+
304+
- name: Archive TMT logs
305+
if: always()
306+
uses: actions/upload-artifact@v7
307+
with:
308+
name: "tmt-log-PR-${{ github.event.number }}-${{ matrix.test_os }}-${{ matrix.variant }}-upgrade-${{ env.ARCH }}"
309+
path: /var/tmp/tmt
310+
265311
# Test bootc install on Fedora CoreOS (separate job to avoid disk space issues
266312
# when run in the same job as test-integration).
267313
# Uses fedora-43 as it's the current stable Fedora release matching CoreOS.
@@ -342,7 +388,7 @@ jobs:
342388
# Sentinel job for required checks - configure this job name in repository settings
343389
required-checks:
344390
if: always()
345-
needs: [cargo-deny, validate, package, test-integration, test-coreos, test-container-export]
391+
needs: [cargo-deny, validate, package, test-integration, test-upgrade, test-coreos, test-container-export]
346392
runs-on: ubuntu-latest
347393
steps:
348394
- run: exit 1
@@ -351,5 +397,6 @@ jobs:
351397
needs.validate.result != 'success' ||
352398
needs.package.result != 'success' ||
353399
needs.test-integration.result != 'success' ||
400+
needs.test-upgrade.result != 'success' ||
354401
needs.test-coreos.result != 'success' ||
355402
needs.test-container-export.result != 'success'

Justfile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
base_img := "localhost/bootc"
1818
# Synthetic upgrade image for testing
1919
upgrade_img := base_img + "-upgrade"
20+
# Base image with tmt dependencies added, used as the boot source for upgrade tests
21+
upgrade_source_img := base_img + "-upgrade-source"
2022

2123
# Build variant: ostree (default) or composefs
2224
variant := env("BOOTC_variant", "ostree")
@@ -141,6 +143,29 @@ test-composefs bootloader filesystem boot_type seal_state *ARGS:
141143
{{ARGS}} \
142144
$(if [ "{{boot_type}}" = "uki" ]; then echo "readonly"; else echo "integration"; fi)
143145

146+
# Run upgrade test: boot VM from published base image (with tmt deps added),
147+
# upgrade to locally-built image, reboot, then run readonly tests to verify.
148+
# The --upgrade-image flag triggers --bind-storage-ro in bcvk, making the
149+
# locally-built image available inside the VM via containers-storage transport.
150+
[group('core')]
151+
test-upgrade *ARGS: build _build-upgrade-source-image
152+
#!/bin/bash
153+
set -xeuo pipefail
154+
composefs_args=()
155+
if [[ "{{variant}}" = composefs ]]; then
156+
composefs_args=(--composefs-backend \
157+
--bootloader={{bootloader}} \
158+
--filesystem={{filesystem}} \
159+
--seal-state={{seal_state}} \
160+
--boot-type={{boot_type}} \
161+
--karg=enforcing=0)
162+
fi
163+
cargo xtask run-tmt --env=BOOTC_variant={{variant}} \
164+
--env=BOOTC_test_upgrade_image={{base_img}} \
165+
--upgrade-image={{base_img}} \
166+
"${composefs_args[@]}" \
167+
{{upgrade_source_img}} {{ARGS}} readonly
168+
144169
# Run cargo fmt and clippy checks in container
145170
[group('core')]
146171
validate:
@@ -339,6 +364,10 @@ _keygen:
339364
_build-upgrade-image:
340365
cat tmt/tests/Dockerfile.upgrade | podman build -t {{upgrade_img}} --from={{base_img}} -
341366

367+
# Build the upgrade source image: base image + tmt dependencies (rsync, nu, cloud-init)
368+
_build-upgrade-source-image:
369+
podman build --build-arg=base={{base}} --build-arg=variant={{variant}} -t {{upgrade_source_img}} -f tmt/tests/Dockerfile.upgrade-source .
370+
342371
# Copy an image from user podman storage to root's podman storage
343372
# This allows building as regular user then running privileged tests
344373
[group('testing')]

crates/xtask/src/tmt.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
22
use camino::{Utf8Path, Utf8PathBuf};
33
use fn_error_context::context;
44
use rand::RngExt;
5-
use xshell::{Shell, cmd};
5+
use xshell::{cmd, Shell};
66

77
// Generation markers for integration.fmf
88
const PLAN_MARKER_BEGIN: &str = "# BEGIN GENERATED PLANS\n";
@@ -492,6 +492,10 @@ pub(crate) fn run_tmt(sh: &Shell, args: &RunTmtArgs) -> Result<()> {
492492
opts.push(format!("--bootloader={b}"));
493493
}
494494

495+
for k in &args.karg {
496+
opts.push(format!("--karg={k}"));
497+
}
498+
495499
opts
496500
};
497501

crates/xtask/src/xtask.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use anyhow::{Context, Result};
1414
use camino::{Utf8Path, Utf8PathBuf};
1515
use clap::{Args, Parser, Subcommand, ValueEnum};
1616
use fn_error_context::context;
17-
use xshell::{Shell, cmd};
17+
use xshell::{cmd, Shell};
1818

1919
mod buildsys;
2020
mod man;
@@ -176,6 +176,10 @@ pub(crate) struct RunTmtArgs {
176176
// Required to send kargs to only bls installs
177177
#[arg(long, default_value_t, requires = "composefs_backend")]
178178
pub(crate) boot_type: BootType,
179+
180+
/// Additional kernel arguments to pass to bcvk
181+
#[arg(long)]
182+
pub(crate) karg: Vec<String>,
179183
}
180184

181185
/// Arguments for tmt-provision command
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Build an image suitable for upgrade testing: takes the published base image
2+
# and adds the minimal dependencies needed by tmt (rsync, nu, etc.)
3+
# so we can boot it in a VM, then upgrade into the locally-built image.
4+
#
5+
# Note: we do NOT pass `cloudinit` to provision-derived.sh because bcvk
6+
# handles SSH key injection itself; cloud-init interferes with that.
7+
ARG base
8+
FROM ${base}
9+
COPY hack/provision-derived.sh hack/packages.txt contrib/packaging/configure-rootfs /run/provision/
10+
ARG variant=ostree
11+
RUN --mount=type=tmpfs,target=/tmp <<EORUN
12+
set -xeuo pipefail
13+
cd /run/provision
14+
# Install nu and tmt dependencies using the same script as the main build,
15+
# but with SKIP_CONFIGS=1 since we don't need LBIs or test kargs in the
16+
# upgrade source image (those come from the image we upgrade into).
17+
SKIP_CONFIGS=1 ./provision-derived.sh
18+
# Ensure a root filesystem type is configured so `bootc install to-disk`
19+
# works for base images that don't ship a default (e.g. Fedora).
20+
# For the ostree variant, configure-rootfs will default to xfs.
21+
./configure-rootfs "${variant}"
22+
# For composefs, rebuild the initramfs to include the bootc dracut module.
23+
# The module's check() returns 255 so it's never auto-included, but it's
24+
# required for composefs root setup during boot.
25+
if [[ "${variant}" == composefs* ]]; then
26+
mkdir -p /etc/dracut.conf.d
27+
echo 'add_dracutmodules+=" bootc "' > /etc/dracut.conf.d/50-bootc-composefs.conf
28+
kver=$(cd /usr/lib/modules && echo *)
29+
dracut --force --kver "$kver" "/usr/lib/modules/$kver/initramfs.img"
30+
fi
31+
rm -rf /run/provision
32+
EORUN

tmt/tests/booted/bootc_testlib.nu

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# A simple nushell "library" for the
1+
# A simple nushell "library" for bootc test helpers
22

33
# This is a workaround for what must be a systemd bug
44
# that seems to have appeared in C10S
@@ -23,3 +23,56 @@ export def have_hostexports [] {
2323
export def parse_cmdline [] {
2424
open /proc/cmdline | str trim | split row " "
2525
}
26+
27+
# If the BOOTC_test_upgrade_image environment variable is set, performs
28+
# an upgrade to that image and reboots on the first boot. On the second
29+
# boot (after the upgrade), verifies we're running the upgraded image
30+
# and returns so the caller can proceed with its tests.
31+
#
32+
# This enables an "upgrade test" flow: boot from a published base image,
33+
# upgrade to the image under test, reboot, then run verification tests.
34+
#
35+
# Note: This uses BOOTC_test_upgrade_image (the image to upgrade *into*),
36+
# which is distinct from BOOTC_upgrade_image (the synthetic upgrade image
37+
# used by existing upgrade tests like test-image-upgrade-reboot).
38+
#
39+
# Returns without doing anything if BOOTC_test_upgrade_image is not set.
40+
export def maybe_upgrade [] {
41+
use std assert
42+
43+
let upgrade_image = $env.BOOTC_test_upgrade_image? | default ""
44+
if $upgrade_image == "" {
45+
return
46+
}
47+
48+
match $env.TMT_REBOOT_COUNT? {
49+
null | "0" => {
50+
if not (have_hostexports) {
51+
error make { msg: "BOOTC_test_upgrade_image is set but host exports (--bind-storage-ro) are not available" }
52+
}
53+
# Save the pre-upgrade bootc version so post-upgrade tests
54+
# can detect known incompatibilities with older versions.
55+
let pre_ver = (bootc --version | parse "bootc {v}" | get 0.v)
56+
$pre_ver | save /var/bootc-pre-upgrade-version
57+
print $"Pre-upgrade bootc version: ($pre_ver)"
58+
59+
print $"Upgrade image specified: ($upgrade_image)"
60+
print "Performing upgrade switch..."
61+
bootc switch --transport containers-storage $upgrade_image
62+
print "Switch complete, rebooting..."
63+
tmt-reboot
64+
},
65+
"1" => {
66+
print $"Second boot after upgrade to ($upgrade_image)"
67+
let st = bootc status --json | from json
68+
let booted = $st.status.booted.image
69+
assert equal $booted.image.transport "containers-storage"
70+
assert equal $booted.image.image $upgrade_image
71+
print "Upgrade verified, continuing with tests..."
72+
},
73+
$o => {
74+
# For higher reboot counts, just continue - the caller
75+
# may have its own reboot logic
76+
},
77+
}
78+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use std assert
2+
use tap.nu
3+
4+
tap begin "verify bootc upgrade --check succeeds after upgrade"
5+
6+
# After an upgrade (bootc switch), verify that the running system can
7+
# still query for further upgrades. This catches regressions where
8+
# on-disk state created by an older bootc is incompatible with the
9+
# new bootc's upgrade machinery (e.g. #2074).
10+
# Only meaningful when we actually performed an upgrade.
11+
let upgrade_image = $env.BOOTC_test_upgrade_image? | default ""
12+
if $upgrade_image == "" {
13+
print "# skip: not an upgrade test (BOOTC_test_upgrade_image not set)"
14+
tap ok
15+
exit 0
16+
}
17+
18+
# Read the pre-upgrade bootc version saved during first boot.
19+
let old_version_str = (open /var/bootc-pre-upgrade-version | str trim)
20+
let old_ver = ($old_version_str | split row "." | each { into int })
21+
print $"Pre-upgrade bootc version: ($old_version_str)"
22+
23+
let is_composefs = (tap is_composefs)
24+
25+
print "Running bootc upgrade --check to verify upgrade machinery works..."
26+
let result = do -i { bootc upgrade --check } | complete
27+
if $result.exit_code == 0 {
28+
print "bootc upgrade --check succeeded"
29+
} else {
30+
print $"bootc upgrade --check failed: ($result.stderr)"
31+
# Known failure: composefs upgrades from bootc <= 1.13 have
32+
# incompatible on-disk state (see #2074).
33+
let old_bootc_le_1_13 = ($old_ver.0 < 1) or (($old_ver.0 == 1) and ($old_ver.1 <= 13))
34+
if $is_composefs and $old_bootc_le_1_13 {
35+
print $"# known failure: composefs upgrade --check from bootc ($old_version_str) \(see #2074\)"
36+
} else {
37+
error make { msg: $"bootc upgrade --check failed unexpectedly: ($result.stderr)" }
38+
}
39+
}
40+
41+
tap ok

tmt/tests/booted/test-01-readonly.nu

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@
77
#
88
# Run all readonly tests in sequence
99
use tap.nu
10+
use bootc_testlib.nu
1011

1112
tap begin "readonly tests"
1213

14+
# If an upgrade image is specified (via BOOTC_test_upgrade_image env var),
15+
# perform the upgrade and reboot first. On the second boot after upgrade,
16+
# this returns and we continue with the readonly tests below.
17+
bootc_testlib maybe_upgrade
18+
1319
# Get all readonly test files and run them in order
1420
let tests = (ls booted/readonly/*-test-*.nu | get name | sort)
1521

0 commit comments

Comments
 (0)