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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,19 @@ jobs:
run: |
set -xeuo pipefail
sudo podman run --rm -v $PWD:/run/src -w /run/src --privileged localhost/bootupd:latest tests/tests/generate-update-metadata.sh

# Verify bootupd works in a bcvk ephemeral (virtiofs) environment.
# This catches regressions where bootloader-update.service fails on
# systems without a disk-backed bootloader (direct kernel boot).
ephemeral:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: bootc-dev/actions/bootc-ubuntu-setup@main
with:
libvirt: true
- name: Build container image
run: sudo podman build --build-arg=base=quay.io/fedora/fedora-bootc:43 -t localhost/bootupd:latest -f Dockerfile .
- name: Smoke test (bcvk ephemeral)
timeout-minutes: 10
run: sudo bcvk ephemeral run-ssh localhost/bootupd:latest -- /usr/libexec/bootupd-tests/ephemeral-test.sh
11 changes: 8 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ COPY --from=build /out/ /
RUN <<EORUN
set -xeuo pipefail
dnf -y install dnf-plugins-core
# dnf5 (used on Fedora 43+) needs the copr plugin installed separately
dnf -y install dnf5-plugins 2>/dev/null || true
dnf -y copr enable rhcontainerbot/bootc centos-stream-9-x86_64
dnf -y install bootc
dnf clean all
Expand All @@ -38,8 +40,11 @@ EORUN
# Remove /var/roothome as workaround
RUN <<EORUN
set -xeuo pipefail
[ -d /var/roothome ] && rm -rf /var/roothome
rm -rf /var/roothome
EORUN
# Sanity check this too
RUN bootc container lint --fatal-warnings
# Install CI test scripts (used by bcvk ephemeral smoke tests)
COPY --from=build /build/ci/ephemeral-test.sh /usr/libexec/bootupd-tests/ephemeral-test.sh
# Sanity check this too; don't use --fatal-warnings as some base images
# have pre-existing warnings (e.g. /run/systemd content in Fedora).
RUN bootc container lint

29 changes: 29 additions & 0 deletions ci/ephemeral-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/bash
# Smoke test for bcvk ephemeral (virtiofs direct-boot) environments.
# This runs *inside* the ephemeral VM and verifies that bootupd
# handles the diskless virtiofs root gracefully.
set -xeuo pipefail

# Verify we're actually on virtiofs — this test is meaningless otherwise.
root_fstype=$(findmnt -n -o FSTYPE /)
if [ "$root_fstype" != "virtiofs" ]; then
echo "ERROR: expected root fstype 'virtiofs', got '${root_fstype}'" >&2
exit 1
fi
echo "ok: root filesystem is virtiofs"

# The bootloader-update.service should have already run at boot (it's
# enabled by preset on Fedora). Verify it succeeded rather than failed.
systemctl is-active bootloader-update.service
echo "ok: bootloader-update.service is active (ran successfully at boot)"

# Also verify a manual invocation skips cleanly.
output=$(bootupctl update 2>&1)
echo "$output"
if ! echo "$output" | grep -qi 'skipping'; then
echo "ERROR: expected skip message in output" >&2
exit 1
fi
echo "ok: bootupctl update skipped cleanly on virtiofs"

echo "All ephemeral smoke tests passed."
38 changes: 27 additions & 11 deletions src/bootupd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,17 +430,19 @@ pub(crate) fn adopt_and_update(
/// Get the block device backing the current root by trying `/boot` first,
/// then falling back to `/sysroot`. This avoids issues with virtual
/// filesystems like composefs that are mounted on `/`.
#[context("Finding block device from boot or sysroot")]
fn list_dev_current_root() -> Result<Device> {
///
/// Returns `Ok(None)` when no block-backed filesystem is found (e.g. virtiofs
/// in bcvk ephemeral, NFS root, ISO boot), so callers can skip gracefully.
fn list_dev_current_root() -> Result<Option<Device>> {
let auth = cap_std::ambient_authority();
for path in ["/boot", "/sysroot"] {
if let Ok(dir) = Dir::open_ambient_dir(path, auth) {
if let Ok(dev) = bootc_internal_blockdev::list_dev_by_dir(&dir) {
return Ok(dev);
return Ok(Some(dev));
}
}
}
anyhow::bail!("Failed to find block device from /boot or /sysroot")
Ok(None)
}

/// daemon implementation of component validate
Expand All @@ -450,7 +452,9 @@ pub(crate) fn validate(name: &str) -> Result<ValidationResult> {
let Some(inst) = state.installed.get(name) else {
anyhow::bail!("Component {} is not installed", name);
};
let device = list_dev_current_root()?;
let Some(device) = list_dev_current_root()? else {
return Ok(ValidationResult::Skip);
};
component.validate(inst, &device)
}

Expand Down Expand Up @@ -612,17 +616,27 @@ impl RootContext {
}
}

/// Initialize parent devices to prepare the update
fn prep_before_update() -> Result<RootContext> {
/// Initialize parent devices to prepare the update.
///
/// Returns `Ok(None)` when no block-backed boot filesystem is found,
/// so the caller can skip the update gracefully.
fn prep_before_update() -> Result<Option<RootContext>> {
let path = "/";
let sysroot = openat::Dir::open(path).context("Opening root dir")?;
let device = list_dev_current_root()?;
Ok(RootContext::new(sysroot, path, device))
let Some(device) = list_dev_current_root()? else {
println!(
"No block-backed boot filesystem found; bootloader update is not applicable, skipping."
);
return Ok(None);
};
Ok(Some(RootContext::new(sysroot, path, device)))
}

pub(crate) fn client_run_update() -> Result<()> {
crate::try_fail_point!("update");
let rootcxt = prep_before_update()?;
let Some(rootcxt) = prep_before_update()? else {
return Ok(());
};
let status: Status = status()?;
if status.components.is_empty() && status.adoptable.is_empty() {
println!("No components installed.");
Expand Down Expand Up @@ -677,7 +691,9 @@ pub(crate) fn client_run_update() -> Result<()> {
}

pub(crate) fn client_run_adopt_and_update(with_static_config: bool) -> Result<()> {
let rootcxt = prep_before_update()?;
let Some(rootcxt) = prep_before_update()? else {
return Ok(());
};
let status: Status = status()?;
if status.adoptable.is_empty() {
println!("No components are adoptable.");
Expand Down
Loading