+++ /dev/null
-name: 'Bootc Ubuntu Setup'
-description: 'Default host setup'
-inputs:
- libvirt:
- description: 'Install libvirt and virtualization stack'
- required: false
- default: 'false'
-runs:
- using: 'composite'
- steps:
- # The default runners have TONS of crud on them...
- - name: Free up disk space on runner
- shell: bash
- run: |
- set -xeuo pipefail
- sudo df -h
- unwanted_pkgs=('^aspnetcore-.*' '^dotnet-.*' '^llvm-.*' 'php.*' '^mongodb-.*' '^mysql-.*'
- azure-cli google-chrome-stable firefox mono-devel)
- unwanted_dirs=(/usr/share/dotnet /opt/ghc /usr/local/lib/android /opt/hostedtoolcache/CodeQL)
- # Start background removal operations as systemd units; if this causes
- # races in the future around disk space we can look at waiting for cleanup
- # before starting further jobs, but right now we spent a lot of time waiting
- # on the network and scripts and such below, giving these plenty of time to run.
- n=0
- runcleanup() {
- sudo systemd-run -r -u action-cleanup-${n} -- "$@"
- n=$(($n + 1))
- }
- runcleanup docker image prune --all --force
- for x in ${unwanted_dirs[@]}; do
- runcleanup rm -rf "$x"
- done
- # Apt removals in foreground, as we can't parallelize these
- for x in ${unwanted_pkgs[@]}; do
- /bin/time -f '%E %C' sudo apt-get remove -y $x
- done
- # We really want support for heredocs
- - name: Update podman and install just
- shell: bash
- run: |
- set -eux
- # Require the runner is ubuntu-24.04
- IDV=$(. /usr/lib/os-release && echo ${ID}-${VERSION_ID})
- test "${IDV}" = "ubuntu-24.04"
- # plucky is the next release
- echo 'deb http://azure.archive.ubuntu.com/ubuntu plucky universe main' | sudo tee /etc/apt/sources.list.d/plucky.list
- /bin/time -f '%E %C' sudo apt update
- # skopeo is currently older in plucky for some reason hence --allow-downgrades
- /bin/time -f '%E %C' sudo apt install -y --allow-downgrades crun/plucky podman/plucky skopeo/plucky just
- # This is the default on e.g. Fedora derivatives, but not Debian
- - name: Enable unprivileged /dev/kvm access
- shell: bash
- run: |
- set -xeuo pipefail
- echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
- sudo udevadm control --reload-rules
- sudo udevadm trigger --name-match=kvm
- ls -l /dev/kvm
- # Used by a few workflows, but generally useful
- - name: Set architecture variable
- id: set_arch
- shell: bash
- run: echo "ARCH=$(arch)" >> $GITHUB_ENV
- # Install libvirt stack if requested
- - name: Install libvirt and virtualization stack
- if: ${{ inputs.libvirt == 'true' }}
- shell: bash
- run: |
- set -xeuo pipefail
- export BCVK_VERSION=0.8.0
- /bin/time -f '%E %C' sudo apt install -y libkrb5-dev pkg-config libvirt-dev genisoimage qemu-utils qemu-kvm virtiofsd libvirt-daemon-system
- # Something in the stack is overriding this, but we want session right now for bcvk
- echo LIBVIRT_DEFAULT_URI=qemu:///session >> $GITHUB_ENV
- td=$(mktemp -d)
- cd $td
- # Install bcvk
- target=bcvk-$(arch)-unknown-linux-gnu
- /bin/time -f '%E %C' curl -LO https://github.com/bootc-dev/bcvk/releases/download/v${BCVK_VERSION}/${target}.tar.gz
- tar xzf ${target}.tar.gz
- sudo install -T ${target} /usr/bin/bcvk
- cd -
- rm -rf "$td"
-
- # Also bump the default fd limit as a workaround for https://github.com/bootc-dev/bcvk/issues/65
- sudo sed -i -e 's,^\* hard nofile 65536,* hard nofile 524288,' /etc/security/limits.conf
- - name: Cleanup status
- shell: bash
- run: |
- set -xeuo pipefail
- systemctl list-units 'action-cleanup*'
- df -h
stream: [stream9, stream10]
steps:
- uses: actions/checkout@v4
- - uses: ./.github/actions/bootc-ubuntu-setup
+ - uses: bootc-dev/actions/bootc-ubuntu-setup@main
+ with:
+ libvirt: true
- name: build
- run: sudo STREAM=${{ matrix.stream }} just build
+ run: STREAM=${{ matrix.stream }} just build
- name: unitcontainer
- run: sudo STREAM=${{ matrix.stream }} just unitcontainer
+ run: STREAM=${{ matrix.stream }} just unitcontainer
- name: unittest
- run: sudo STREAM=${{ matrix.stream }} just unittest
- - name: bootc install
- run: |
- set -xeuo pipefail
- sudo podman run --env BOOTC_SKIP_SELINUX_HOST_CHECK=1 --rm -ti --privileged -v /:/target --pid=host --security-opt label=disable \
- -v /dev:/dev -v /var/lib/containers:/var/lib/containers \
- localhost/ostree:latest bootc install to-filesystem --skip-fetch-check \
- --replace=alongside /target
- # Verify labeling for /etc
- sudo ls -dZ /ostree/deploy/default/deploy/*.0/etc |grep :etc_t:
+ run: STREAM=${{ matrix.stream }} just unittest
+ - name: integration tests (bcvk)
+ timeout-minutes: 15
+ run: STREAM=${{ matrix.stream }} just integration-container
- name: Upload test logs
if: failure()
uses: actions/upload-artifact@v4
- run: sudo apt install clang-format just
- name: Test style
run: just clang-format-check
- build-integration:
- runs-on: ubuntu-latest
- container: quay.io/coreos-assembler/fcos-buildroot:testing-devel
- steps:
- - uses: actions/checkout@v3
- - name: Cache Dependencies
- uses: Swatinem/rust-cache@ce325b60658c1b38465c06cc965b79baf32c1e72
- with:
- key: "integration"
- - name: Build
- run: cd tests/inst && cargo build --verbose --release
- - name: Upload binary
- uses: actions/upload-artifact@v4
- with:
- name: ostree-test
- path: tests/inst/target/release/ostree-test
minimal:
name: "Build - FCOS minimal"
runs-on: ubuntu-latest
with:
name: inst.tar.zst
path: inst.tar.zst
- privtest:
- name: "Privileged testing"
- needs: [build-c, build-integration]
- runs-on: ubuntu-latest
- container:
- image: quay.io/fedora/fedora-coreos:testing-devel
- options: "--privileged --pid=host -v /run/systemd:/run/systemd -v /:/run/host"
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
- - name: Download install tree
- uses: actions/download-artifact@v4
- with:
- name: inst.tar.zst
- - name: Install
- run: tar -C / -xvf inst.tar.zst && rm -f inst.tar.zst
- - name: Download test binary
- uses: actions/download-artifact@v4
- with:
- name: ostree-test
- - name: Install
- run: install ostree-test /usr/bin
- - name: Setup
- # https://github.com/ostreedev/ostree-rs-ext/issues/417
- run: mkdir -p /var/tmp
- - name: Integration tests (unit)
- run: ostree-test
tests:
# Distro configuration matrix
#
COPY --from=build /out/ /
COPY --from=src /src/tests-unit-container /tests
+# Build integration test binary
+FROM $base as integration-build
+RUN dnf -y install cargo rust
+COPY tests/bootc-integration/Cargo.toml tests/bootc-integration/Cargo.lock /build/tests/bootc-integration/
+WORKDIR /build/tests/bootc-integration
+RUN --mount=type=cache,target=/root/.cargo/registry \
+ --mount=type=cache,target=/root/.cargo/git \
+ cargo fetch
+COPY tests/bootc-integration /build/tests/bootc-integration
+RUN --network=none \
+ --mount=type=cache,target=/root/.cargo/registry \
+ --mount=type=cache,target=/root/.cargo/git \
+ --mount=type=cache,target=/build/tests/bootc-integration/target \
+ cargo build --release && \
+ cp target/release/ostree-bootc-integration-tests /usr/bin/ostree-bootc-integration-tests
+
# Override userspace
FROM $base as rootfs
# Remove the default binaries to ensure we're getting our overrides
# The default final container, with also a regenerated
# initramfs in case ostree-prepare-root changed.
FROM rootfs
+COPY --from=integration-build /usr/bin/ostree-bootc-integration-tests /usr/bin/ostree-bootc-integration-tests
# https://docs.fedoraproject.org/en-US/bootc/initramfs/#_regenerating_the_initrd
# since we have ostree-prepare-root there
RUN set -x; kver=$(cd /usr/lib/modules && echo *); dracut -vf /usr/lib/modules/$kver/initramfs.img $kver
make -C target/c install DESTDIR=$(pwd)/target/inst
tar --sort=name --numeric-owner --owner=0 --group=0 -C target/inst -czf target/inst.tar.gz .
+# Run all integration tests inside a deployed libvirt VM.
+# This runs both "booted" tests (need full deployment) and "privileged"
+# tests (just need root). For faster iteration on privileged-only tests,
+# use `integration-ephemeral`.
+# JUnit XML results are written to target/integration-results.xml.
+integration-container *ARGS:
+ #!/bin/bash
+ set -euo pipefail
+ bcvk libvirt run --name ostree-integration-test --replace --detach --ssh-wait localhost/ostree:latest
+ rc=0
+ bcvk libvirt ssh ostree-integration-test -- env JUNIT_OUTPUT=/tmp/junit.xml \
+ ostree-bootc-integration-tests {{ARGS}} || rc=$?
+ mkdir -p target
+ bcvk libvirt ssh ostree-integration-test -- cat /tmp/junit.xml \
+ > target/integration-results.xml 2>/dev/null || true
+ bcvk libvirt rm --stop --force ostree-integration-test
+ exit $rc
+
+# Run only root-privileged tests via an ephemeral VM (faster — no disk install).
+# This skips tests that need a fully deployed/booted ostree system.
+# Not all tests will pass here; use `integration-container` for the full suite.
+integration-ephemeral *ARGS:
+ bcvk ephemeral run-ssh localhost/ostree:latest -- ostree-bootc-integration-tests {{ARGS}}
+
+# Remove any leftover integration test VMs
+integration-cleanup:
+ -bcvk libvirt rm --stop --force ostree-integration-test
+
sourcefiles := "git ls-files '**.c' '**.cxx' '**.h' '**.hpp'"
# Reformat source files
clang-format:
--- /dev/null
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "anstream"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "escape8259"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6"
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itoa"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+
+[[package]]
+name = "js-sys"
+version = "0.3.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.182"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
+
+[[package]]
+name = "libtest-mimic"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap",
+ "escape8259",
+]
+
+[[package]]
+name = "linkme"
+version = "0.3.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e3283ed2d0e50c06dd8602e0ab319bb048b6325d0bba739db64ed8205179898"
+dependencies = [
+ "linkme-impl",
+]
+
+[[package]]
+name = "linkme-impl"
+version = "0.3.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5cec0ec4228b4853bb129c84dbf093a27e6c7a20526da046defc334a1b017f7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "newtype-uuid"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c012d14ef788ab066a347d19e3dda699916c92293b05b85ba2c76b8c82d2830"
+dependencies = [
+ "uuid",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "ostree-bootc-integration-tests"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "libtest-mimic",
+ "linkme",
+ "paste",
+ "quick-junit",
+ "rustix",
+ "tempfile",
+ "xshell",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-junit"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ee9342d671fae8d66b3ae9fd7a9714dfd089c04d2a8b1ec0436ef77aee15e5f"
+dependencies = [
+ "chrono",
+ "indexmap",
+ "newtype-uuid",
+ "quick-xml",
+ "strip-ansi-escapes",
+ "thiserror",
+ "uuid",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.38.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "strip-ansi-escapes"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025"
+dependencies = [
+ "vte",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0"
+dependencies = [
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "vte"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wasip2"
+version = "1.0.2+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "xshell"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e7290c623014758632efe00737145b6867b66292c42167f2ec381eb566a373d"
+dependencies = [
+ "xshell-macros",
+]
+
+[[package]]
+name = "xshell-macros"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
--- /dev/null
+[package]
+name = "ostree-bootc-integration-tests"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+# Separate workspace from root; this is built inside the container
+[workspace]
+
+[[bin]]
+name = "ostree-bootc-integration-tests"
+path = "src/main.rs"
+
+[dependencies]
+anyhow = "1"
+libtest-mimic = "0.8"
+linkme = "0.3"
+paste = "1"
+quick-junit = "0.5"
+rustix = { version = "1", default-features = false, features = ["process"] }
+tempfile = "3"
+xshell = "0.2"
--- /dev/null
+# Bootc integration tests
+
+Integration tests that run inside a [bcvk](https://github.com/bootc-dev/bcvk) VM
+against a deployed ostree system. The test binary is baked into the container
+image at build time and executed over SSH.
+
+Tests are split into two tiers: `booted_test!` tests need a fully deployed
+system (via `bcvk libvirt run`) while `privileged_test!` tests just need root
+and can run in a faster ephemeral VM. See the module docs in
+`src/tests/privileged.rs` for details.
+
+## Running
+
+```sh
+just build # build the container image
+just integration-container # run all tests in a deployed VM
+just integration-ephemeral # fast path: privileged-only tests
+just integration-container --exact privileged_verify_composefs # single test
+just integration-container --list # list tests
+just integration-cleanup # remove leftover VMs
+```
+
+Set `JUNIT_OUTPUT` to a file path to get JUnit XML results. The
+`integration-container` recipe does this automatically, writing to
+`target/integration-results.xml`.
--- /dev/null
+//! Integration test infrastructure for ostree bootc testing.
+//!
+//! Provides test registration via [`linkme`] distributed slices and the
+//! [`integration_test!`] macro, collected and executed by a custom
+//! [`libtest_mimic`] harness in `main.rs`.
+
+// linkme requires unsafe for distributed slices
+#![allow(unsafe_code)]
+
+/// A test function that returns a Result.
+pub type TestFn = fn() -> anyhow::Result<()>;
+
+/// Metadata for a registered integration test.
+#[derive(Debug)]
+pub struct IntegrationTest {
+ /// Name of the integration test.
+ pub name: &'static str,
+ /// Test function to execute.
+ pub f: TestFn,
+}
+
+impl IntegrationTest {
+ /// Create a new integration test with the given name and function.
+ pub const fn new(name: &'static str, f: TestFn) -> Self {
+ Self { name, f }
+ }
+}
+
+/// Distributed slice holding all registered integration tests.
+#[linkme::distributed_slice]
+pub static INTEGRATION_TESTS: [IntegrationTest];
+
+/// Register an integration test function.
+///
+/// # Examples
+///
+/// ```ignore
+/// fn test_something() -> anyhow::Result<()> {
+/// Ok(())
+/// }
+/// integration_test!(test_something);
+/// ```
+#[macro_export]
+macro_rules! integration_test {
+ ($fn_name:ident) => {
+ ::paste::paste! {
+ #[::linkme::distributed_slice($crate::INTEGRATION_TESTS)]
+ static [<$fn_name:upper>]: $crate::IntegrationTest =
+ $crate::IntegrationTest::new(stringify!($fn_name), $fn_name);
+ }
+ };
+}
--- /dev/null
+//! Integration test runner for ostree bootc testing.
+//!
+//! This binary uses [`libtest_mimic`] as a custom test harness.
+//! Tests are registered via the [`integration_test!`] macro in submodules
+//! and collected from the [`INTEGRATION_TESTS`] distributed slice at startup.
+//!
+//! When the `JUNIT_OUTPUT` environment variable is set to a file path,
+//! JUnit XML results are written there after tests complete.
+
+// linkme requires unsafe for distributed slices
+#![allow(unsafe_code)]
+
+use std::sync::{Arc, Mutex};
+use std::time::Instant;
+
+use libtest_mimic::{Arguments, Trial};
+
+pub(crate) use ostree_bootc_integration_tests::{integration_test, INTEGRATION_TESTS};
+
+mod tests;
+
+/// Outcome of a single test, recorded during execution.
+struct TestOutcome {
+ name: String,
+ duration: std::time::Duration,
+ result: Result<(), String>,
+}
+
+fn main() {
+ let args = Arguments::from_args();
+ let outcomes: Arc<Mutex<Vec<TestOutcome>>> = Arc::new(Mutex::new(Vec::new()));
+
+ let tests: Vec<Trial> = INTEGRATION_TESTS
+ .iter()
+ .map(|t| {
+ let f = t.f;
+ let name = t.name.to_owned();
+ let outcomes = Arc::clone(&outcomes);
+ Trial::test(t.name, move || {
+ let start = Instant::now();
+ let result = f();
+ let duration = start.elapsed();
+ let outcome = TestOutcome {
+ name,
+ duration,
+ result: result.as_ref().map(|_| ()).map_err(|e| format!("{e:?}")),
+ };
+ outcomes.lock().unwrap().push(outcome);
+ result.map_err(|e| format!("{e:?}").into())
+ })
+ })
+ .collect();
+
+ let conclusion = libtest_mimic::run(&args, tests);
+
+ // Write JUnit XML if requested
+ if let Ok(path) = std::env::var("JUNIT_OUTPUT") {
+ if let Err(e) = write_junit(&path, &outcomes.lock().unwrap()) {
+ eprintln!("warning: failed to write JUnit XML to {path}: {e}");
+ }
+ }
+
+ std::process::exit(if conclusion.has_failed() { 101 } else { 0 });
+}
+
+fn write_junit(path: &str, outcomes: &[TestOutcome]) -> anyhow::Result<()> {
+ use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
+
+ let mut report = Report::new("ostree-bootc-integration-tests");
+ let mut suite = TestSuite::new("privileged");
+
+ for outcome in outcomes {
+ let status = match &outcome.result {
+ Ok(()) => TestCaseStatus::success(),
+ Err(msg) => {
+ let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
+ status.set_message(msg.clone());
+ status
+ }
+ };
+ let mut tc = TestCase::new(outcome.name.clone(), status);
+ tc.set_time(outcome.duration);
+ suite.add_test_case(tc);
+ }
+
+ report.add_test_suite(suite);
+ let xml = report
+ .to_string()
+ .map_err(|e| anyhow::anyhow!("JUnit serialization failed: {e}"))?;
+ std::fs::write(path, xml)?;
+ eprintln!("JUnit XML written to {path}");
+ Ok(())
+}
--- /dev/null
+//! Integration test modules.
+
+pub mod privileged;
--- /dev/null
+//! Integration tests requiring root privileges.
+//!
+//! Tests are split into two categories:
+//!
+//! * **Booted** (`booted_test!`) — need a fully deployed ostree system
+//! (composefs, sysroot, boot markers). When not root these dispatch via
+//! `bcvk libvirt run` which does a full `bootc install to-disk`.
+//!
+//! * **Privileged** (`privileged_test!`) — just need root and the ostree
+//! binary installed. When not root these dispatch via the much faster
+//! `bcvk ephemeral run-ssh` (no disk install).
+
+use anyhow::{ensure, Result};
+use xshell::{cmd, Shell};
+
+use crate::integration_test;
+
+/// How a test should be dispatched when not running as root.
+/// See the module-level docs for details on each mode.
+enum RunMode {
+ Booted,
+ Privileged,
+}
+
+macro_rules! booted_test {
+ ($fn_name:ident, $body:expr) => {
+ fn $fn_name() -> Result<()> {
+ if require_root(stringify!($fn_name), RunMode::Booted)?.is_some() {
+ return Ok(());
+ }
+ $body
+ }
+ integration_test!($fn_name);
+ };
+}
+
+macro_rules! privileged_test {
+ ($fn_name:ident, $body:expr) => {
+ fn $fn_name() -> Result<()> {
+ if require_root(stringify!($fn_name), RunMode::Privileged)?.is_some() {
+ return Ok(());
+ }
+ $body
+ }
+ integration_test!($fn_name);
+ };
+}
+
+/// Returns `Ok(None)` if already root (test proceeds normally).
+/// Otherwise dispatches to a VM per `mode` and returns `Ok(Some(()))`.
+fn require_root(test_name: &str, mode: RunMode) -> Result<Option<()>> {
+ if rustix::process::getuid().is_root() {
+ return Ok(None);
+ }
+
+ // We're on the host without root — delegate to a VM.
+ if std::env::var_os("OSTREE_IN_VM").is_some() {
+ anyhow::bail!("OSTREE_IN_VM is set but we're not root — VM setup is broken");
+ }
+
+ let image = std::env::var("OSTREE_TEST_IMAGE").map_err(|_| {
+ anyhow::anyhow!(
+ "not root and OSTREE_TEST_IMAGE not set; \
+ run `just integration-container` to build and test"
+ )
+ })?;
+
+ let sh = Shell::new()?;
+ let bcvk = std::env::var("BCVK_PATH").unwrap_or_else(|_| "bcvk".into());
+
+ match mode {
+ RunMode::Booted => {
+ // Use a unique VM name per test to avoid collisions
+ let vm_name = format!("ostree-test-{}", test_name.replace('_', "-"));
+
+ // Deploy a full VM (bootc install to-disk + boot) and wait for SSH
+ cmd!(
+ sh,
+ "{bcvk} libvirt run --name {vm_name} --replace --detach --ssh-wait {image}"
+ )
+ .run()?;
+
+ // Run the test inside the deployed VM
+ let result = cmd!(
+ sh,
+ "{bcvk} libvirt ssh {vm_name} -- ostree-bootc-integration-tests --exact {test_name}"
+ )
+ .run();
+
+ // Always clean up the VM
+ let _ = cmd!(sh, "{bcvk} libvirt rm --stop --force {vm_name}").run();
+
+ // Propagate the test result
+ result?;
+ }
+ RunMode::Privileged => {
+ // Fast path: ephemeral container, no disk install needed
+ cmd!(
+ sh,
+ "{bcvk} ephemeral run-ssh {image} -- ostree-bootc-integration-tests --exact {test_name}"
+ )
+ .run()?;
+ }
+ }
+
+ Ok(Some(()))
+}
+
+booted_test!(privileged_verify_ostree_booted, {
+ let sh = Shell::new()?;
+
+ // /run/ostree-booted is the canonical marker
+ ensure!(
+ std::path::Path::new("/run/ostree-booted").exists(),
+ "/run/ostree-booted does not exist — system was not booted via ostree"
+ );
+
+ // ostree admin status should succeed and show a deployment
+ let status = cmd!(sh, "ostree admin status").read()?;
+ ensure!(
+ !status.is_empty(),
+ "ostree admin status returned empty output"
+ );
+
+ Ok(())
+});
+
+booted_test!(privileged_verify_sysroot, {
+ ensure!(
+ std::path::Path::new("/sysroot").is_dir(),
+ "/sysroot does not exist"
+ );
+ ensure!(
+ std::path::Path::new("/ostree").is_dir(),
+ "/ostree does not exist"
+ );
+ ensure!(
+ std::path::Path::new("/ostree/repo").is_dir(),
+ "/ostree/repo does not exist"
+ );
+
+ // Verify there's at least one deployment
+ let deploy_dir = std::path::Path::new("/ostree/deploy");
+ ensure!(deploy_dir.is_dir(), "/ostree/deploy does not exist");
+
+ let has_stateroot = std::fs::read_dir(deploy_dir)?
+ .any(|entry| entry.is_ok_and(|e| e.file_type().is_ok_and(|ft| ft.is_dir())));
+ ensure!(
+ has_stateroot,
+ "no stateroot directories found under /ostree/deploy"
+ );
+
+ Ok(())
+});
+
+booted_test!(privileged_verify_composefs, {
+ let sh = Shell::new()?;
+
+ // With composefs enabled, the root filesystem should be an overlay
+ let fstype = cmd!(sh, "findmnt -n -o FSTYPE /").read()?;
+ ensure!(
+ fstype.trim() == "overlay",
+ "expected root filesystem type 'overlay' (composefs), got '{}'",
+ fstype.trim()
+ );
+
+ // The composefs private directory should exist with restricted permissions
+ let private_dir = std::path::Path::new("/run/ostree/.private");
+ ensure!(
+ private_dir.exists(),
+ "/run/ostree/.private does not exist — composefs may not be active"
+ );
+
+ let metadata = std::fs::metadata(private_dir)?;
+ let mode = std::os::unix::fs::PermissionsExt::mode(&metadata.permissions()) & 0o777;
+ ensure!(
+ mode == 0,
+ "/run/ostree/.private has mode {mode:#o}, expected 0o000"
+ );
+
+ Ok(())
+});
+
+booted_test!(privileged_verify_ostree_cli, {
+ let sh = Shell::new()?;
+
+ // Verify ostree binary works and reports a version
+ let version_output = cmd!(sh, "ostree --version").read()?;
+ ensure!(
+ version_output.contains("libostree"),
+ "ostree --version output doesn't mention libostree: {version_output}"
+ );
+
+ // ostree admin status should succeed
+ let status = cmd!(sh, "ostree admin status").read()?;
+ ensure!(
+ !status.is_empty(),
+ "ostree admin status returned empty output"
+ );
+
+ // ostree refs should succeed (may be empty, but shouldn't error)
+ cmd!(sh, "ostree refs --repo=/ostree/repo").run()?;
+
+ Ok(())
+});
+
+booted_test!(privileged_verify_sysroot_readonly, {
+ let sh = Shell::new()?;
+ let options = cmd!(sh, "findmnt -n -o OPTIONS /sysroot").read()?;
+ ensure!(
+ options.contains("ro"),
+ "/sysroot is not mounted read-only: {options}"
+ );
+ Ok(())
+});
+
+booted_test!(privileged_verify_ostree_run_metadata, {
+ let metadata = std::fs::metadata("/run/ostree")?;
+ let mode = std::os::unix::fs::PermissionsExt::mode(&metadata.permissions()) & 0o777;
+ ensure!(
+ mode == 0o755,
+ "/run/ostree has mode {mode:#o}, expected 0o755"
+ );
+ Ok(())
+});
+
+booted_test!(privileged_verify_immutable_bit, {
+ let sh = Shell::new()?;
+ // If composefs (overlay), the immutable bit doesn't apply
+ let fstype = cmd!(sh, "findmnt -n -o FSTYPE /").read()?;
+ if fstype.trim() == "overlay" {
+ return Ok(());
+ }
+ // https://bugzilla.redhat.com/show_bug.cgi?id=1867601
+ let lsattr_out = cmd!(sh, "lsattr -d /").read()?;
+ ensure!(
+ lsattr_out.contains("-i-"),
+ "immutable bit not set on /: {lsattr_out}"
+ );
+ Ok(())
+});
+
+booted_test!(privileged_verify_osinit_unshare, {
+ let sh = Shell::new()?;
+ // Create a test stateroot; this should not disturb /run/ostree permissions
+ cmd!(sh, "ostree admin os-init ostreetestsuite").run()?;
+ let metadata = std::fs::metadata("/run/ostree")?;
+ let mode = std::os::unix::fs::PermissionsExt::mode(&metadata.permissions()) & 0o777;
+ ensure!(
+ mode == 0o755,
+ "/run/ostree has mode {mode:#o} after os-init, expected 0o755"
+ );
+ Ok(())
+});
+
+privileged_test!(privileged_verify_nofifo, {
+ let sh = Shell::new()?;
+ let tmp = tempfile::tempdir()?;
+ let tmpdir = tmp.path();
+ let _guard = sh.push_dir(tmpdir);
+ cmd!(sh, "ostree --repo=repo init --mode=archive").run()?;
+ cmd!(sh, "mkdir tmproot").run()?;
+ cmd!(sh, "mkfifo tmproot/afile").run()?;
+ let result = cmd!(
+ sh,
+ "ostree --repo=repo commit -b fifotest -s 'commit fifo' --tree=dir=./tmproot"
+ )
+ .ignore_status()
+ .output()?;
+ ensure!(
+ !result.status.success(),
+ "committing a FIFO should have failed"
+ );
+ let stderr = String::from_utf8_lossy(&result.stderr);
+ ensure!(
+ stderr.contains("Not a regular file or symlink"),
+ "expected 'Not a regular file or symlink' in stderr, got: {stderr}"
+ );
+ Ok(())
+});
+
+privileged_test!(privileged_verify_mtime, {
+ let sh = Shell::new()?;
+ let tmp = tempfile::tempdir()?;
+ let tmpdir = tmp.path();
+ let _guard = sh.push_dir(tmpdir);
+ cmd!(sh, "ostree --repo=repo init --mode=archive").run()?;
+ cmd!(sh, "mkdir tmproot").run()?;
+ cmd!(sh, "bash -c 'echo afile > tmproot/afile'").run()?;
+ cmd!(sh, "ostree --repo=repo commit -b test --tree=dir=tmproot")
+ .ignore_stdout()
+ .run()?;
+ let ts_before = tmpdir.join("repo").metadata()?.modified()?;
+ std::thread::sleep(std::time::Duration::from_secs(1));
+ cmd!(
+ sh,
+ "ostree --repo=repo commit -b test -s 'bump mtime' --tree=dir=tmproot"
+ )
+ .ignore_stdout()
+ .run()?;
+ let ts_after = tmpdir.join("repo").metadata()?.modified()?;
+ ensure!(
+ ts_before != ts_after,
+ "repo mtime did not change after second commit"
+ );
+ Ok(())
+});
+
+privileged_test!(privileged_verify_extensions, {
+ let sh = Shell::new()?;
+ let tmp = tempfile::tempdir()?;
+ let tmpdir = tmp.path();
+ let _guard = sh.push_dir(tmpdir);
+ cmd!(sh, "ostree --repo=repo init --mode=bare").run()?;
+ let extensions = tmpdir.join("repo/extensions");
+ ensure!(
+ extensions.exists(),
+ "repo/extensions directory was not created by ostree init"
+ );
+ Ok(())
+});
+
+booted_test!(privileged_verify_selinux_labels, {
+ let sh = Shell::new()?;
+
+ // Check that /etc has the correct SELinux type
+ let ls_output = cmd!(sh, "ls -dZ /etc").read()?;
+ if !ls_output.contains(':') {
+ // SELinux is likely disabled; skip this check
+ return Ok(());
+ }
+ ensure!(
+ ls_output.contains(":etc_t:"),
+ "/etc does not have :etc_t: SELinux label: {ls_output}"
+ );
+
+ Ok(())
+});