Skip to content

Commit 83a7c9f

Browse files
committed
composefs: Support transient /etc, transient root, and volatile /var
Add TOML configuration (setup-root-conf.toml) for composefs mount behaviour: - [root] transient = true: wrap the composefs in a tmpfs overlay; all writes are discarded on reboot. - [etc] mount = transient|overlay|bind|none: control how /etc is mounted from the deployment state directory. - [var] mount = none|bind: control whether /var is bind-mounted from state. When mount = none, /var is left as an empty composefs directory. bootc-root-setup also detects the systemd.volatile=state kernel argument at boot time and automatically skips the /var state bind-mount when it is set, leaving /var empty for systemd-fstab-generator to mount a fresh tmpfs there at local-fs.target. This is the recommended way to get an ephemeral /var: it uses a plain tmpfs rather than overlayfs, which is compatible with tools like podman that use overlayfs under /var/lib/containers. Add inject-baseconfig CI helper, a test-baseconfigs CI job, and a 040-test-baseconfigs.nu integration test that boots each configuration in a VM and validates filesystem types, writability, SELinux labels, and podman graph driver compatibility. Assisted-by: OpenCode (claude-sonnet-4-6@default) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent aef71bf commit 83a7c9f

9 files changed

Lines changed: 464 additions & 58 deletions

File tree

.github/workflows/ci.yml

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,8 @@ jobs:
220220
bootloader: ["grub", "systemd"]
221221
boot_type: ["bls", "uki"]
222222
seal_state: ["sealed", "unsealed"]
223-
224223
exclude:
225-
# centos-9 fails with EUCLEAN (https://github.com/bootc-dev/bootc/issues/1812)
226-
# See: https://github.com/bootc-dev/bcvk/pull/204
224+
# https://github.com/bootc-dev/bootc/issues/1812
227225
- test_os: centos-9
228226
variant: composefs
229227
- seal_state: "sealed"
@@ -385,6 +383,79 @@ jobs:
385383
name: "tmt-log-${{ matrix.test_os }}-${{ matrix.variant }}-upgrade-${{ env.ARCH }}"
386384
path: /var/tmp/tmt
387385

386+
# Test readonly behaviour with baseconfigs (transient mounts) baked into the image.
387+
# Composefs-only: setup-root-conf.toml is a composefs concept; ostree uses a
388+
# different config format (prepare-root.conf) and is not covered here.
389+
# Runs once per distro × baseconfig — no bootloader/filesystem/boot_type matrix.
390+
test-baseconfigs:
391+
if: needs.compute-ci-level.outputs.run_heavy == 'true'
392+
needs: [compute-ci-level, package]
393+
strategy:
394+
fail-fast: false
395+
matrix:
396+
test_os: ${{ fromJson(needs.compute-ci-level.outputs.integration_os_matrix) }}
397+
baseconfigs: ["etc-transient", "root-transient", "var-volatile"]
398+
exclude:
399+
# centos-9 ships an older dracut that lacks the auto-install of setup-root-conf.toml
400+
- test_os: centos-9
401+
402+
runs-on: ubuntu-24.04
403+
404+
steps:
405+
- uses: actions/checkout@v6
406+
- name: Bootc Ubuntu Setup
407+
uses: bootc-dev/actions/bootc-ubuntu-setup@main
408+
with:
409+
libvirt: true
410+
- name: Install tmt
411+
run: pip install --user "tmt[provision-virtual]"
412+
413+
- name: Setup env
414+
run: |
415+
BASE=$(just pullspec-for-os base ${{ matrix.test_os }})
416+
echo "BOOTC_base=${BASE}" >> $GITHUB_ENV
417+
echo "BOOTC_variant=composefs" >> $GITHUB_ENV
418+
echo "BOOTC_baseconfigs=${{ matrix.baseconfigs }}" >> $GITHUB_ENV
419+
echo "RUST_BACKTRACE=full" >> $GITHUB_ENV
420+
421+
- name: Download package artifacts
422+
uses: actions/download-artifact@v8
423+
with:
424+
name: packages-${{ matrix.test_os }}
425+
path: target/packages/
426+
427+
- name: Build container with baseconfig
428+
run: BOOTC_SKIP_PACKAGE=1 just build
429+
430+
- name: Build upgrade image
431+
run: just _build-upgrade-image
432+
433+
- name: Run TMT readonly tests
434+
run: |
435+
cargo xtask run-tmt \
436+
--env=BOOTC_variant=composefs \
437+
--env=BOOTC_baseconfigs=${{ matrix.baseconfigs }} \
438+
--composefs-backend --bootloader=grub --filesystem=ext4 \
439+
--seal-state=unsealed --boot-type=bls \
440+
--upgrade-image=localhost/bootc-upgrade \
441+
localhost/bootc readonly
442+
just clean-local-images
443+
444+
- name: Disk usage summary
445+
if: always()
446+
run: |
447+
echo "### Disk usage" >> "$GITHUB_STEP_SUMMARY"
448+
echo '```' >> "$GITHUB_STEP_SUMMARY"
449+
df -h >> "$GITHUB_STEP_SUMMARY"
450+
echo '```' >> "$GITHUB_STEP_SUMMARY"
451+
452+
- name: Archive TMT logs
453+
if: always()
454+
uses: actions/upload-artifact@v7
455+
with:
456+
name: "tmt-log-${{ matrix.test_os }}-composefs-baseconfigs-${{ matrix.baseconfigs }}-${{ env.ARCH }}"
457+
path: /var/tmp/tmt
458+
388459
# Test bootc install on Fedora CoreOS (separate job to avoid disk space issues
389460
# when run in the same job as test-integration).
390461
# Uses fedora-43 as it's the current stable Fedora release matching CoreOS.
@@ -471,7 +542,7 @@ jobs:
471542
# Accepts 'skipped' as success so that merge_group-only jobs don't block PRs.
472543
required-checks:
473544
if: always()
474-
needs: [compute-ci-level, cargo-deny, validate, install-tests, docs, package, test-integration, test-upgrade, test-container-export]
545+
needs: [compute-ci-level, cargo-deny, validate, install-tests, docs, package, test-integration, test-upgrade, test-baseconfigs, test-container-export]
475546
runs-on: ubuntu-latest
476547
steps:
477548
- name: Check all jobs

Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ FROM base as base-penultimate
222222
ARG variant
223223
ARG bootloader
224224
ARG boot_type
225+
ARG baseconfigs=""
225226

226227
# Switch to a signed systemd-boot, if configured
227228
RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \
@@ -251,6 +252,10 @@ ARG rootfs=""
251252
RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \
252253
--mount=type=bind,from=packaging,src=/,target=/run/packaging \
253254
/run/packaging/configure-rootfs "${variant}" "${rootfs}"
255+
# Inject base configuration (e.g. transient-etc, transient-root) before dracut runs
256+
RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \
257+
--mount=type=bind,from=packaging,src=/,target=/run/packaging \
258+
/run/packaging/inject-baseconfig "${variant}" "${baseconfigs}"
254259
# Override with our built package
255260
RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \
256261
--mount=type=bind,from=packaging,src=/,target=/run/packaging \

Justfile

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ filesystem := env("BOOTC_filesystem", "ext4")
4343
boot_type := env("BOOTC_boot_type", "bls")
4444
# Only used for composefs tests
4545
seal_state := env("BOOTC_seal_state", "unsealed")
46+
# Baseconfigs to inject into the image for testing (e.g. "etc-transient" or "root-transient")
47+
baseconfigs := env("BOOTC_baseconfigs", "")
4648
# Base container image to build from
4749
base := env("BOOTC_base", "quay.io/centos-bootc/centos-bootc:stream10")
4850
# Buildroot base image
@@ -56,18 +58,21 @@ no_auto_local_deps := env("BOOTC_no_auto_local_deps", "")
5658
# Internal variables
5759
nocache := env("BOOTC_nocache", "")
5860
_nocache_arg := if nocache != "" { "--no-cache" } else { "" }
61+
_baseconfigs_env := if baseconfigs != "" { "--env=BOOTC_baseconfigs=" + baseconfigs } else { "" }
5962
testimage_label := "bootc.testimage=1"
6063
lbi_images := "quay.io/curl/curl:latest quay.io/curl/curl-base:latest registry.access.redhat.com/ubi9/podman:latest"
6164
fedora-coreos := "quay.io/fedora/fedora-coreos:testing-devel"
6265
generic_buildargs := ""
6366
_extra_src_args := if extra_src != "" { "-v " + extra_src + ":/run/extra-src:ro --security-opt=label=disable" } else { "" }
67+
# filesystem arg: required for bootc container ukify to allow missing fsverity
6468
base_buildargs := generic_buildargs + " " + _extra_src_args \
6569
+ " --build-arg=base=" + base \
6670
+ " --build-arg=variant=" + variant \
6771
+ " --build-arg=bootloader=" + bootloader \
6872
+ " --build-arg=boot_type=" + boot_type \
6973
+ " --build-arg=seal_state=" + seal_state \
70-
+ " --build-arg=filesystem=" + filesystem # required for bootc container ukify to allow missing fsverity
74+
+ " --build-arg=filesystem=" + filesystem \
75+
+ " --build-arg=baseconfigs=" + baseconfigs
7176
buildargs := base_buildargs \
7277
+ " --cap-add=all --security-opt=label=type:container_runtime_t --device /dev/fuse" \
7378
+ " --secret=id=secureboot_key,src=target/test-secureboot/db.key --secret=id=secureboot_cert,src=target/test-secureboot/db.crt"
@@ -266,7 +271,31 @@ test-container-export: build
266271
# Run tmt tests without rebuilding (for fast iteration)
267272
[group('testing')]
268273
test-tmt-nobuild *ARGS:
269-
cargo xtask run-tmt --env=BOOTC_variant={{variant}} --upgrade-image={{upgrade_img}} {{base_img}} {{ARGS}}
274+
cargo xtask run-tmt --env=BOOTC_variant={{variant}} {{_baseconfigs_env}} --upgrade-image={{upgrade_img}} {{base_img}} {{ARGS}}
275+
276+
# Run readonly tests with a baseconfig baked into the image at build time.
277+
# Requires composefs variant. Example: just variant=composefs test-tmt-baseconfig root-transient
278+
[group('testing')]
279+
test-tmt-baseconfig baseconfig *ARGS:
280+
just variant=composefs baseconfigs={{baseconfig}} build
281+
just variant=composefs baseconfigs={{baseconfig}} _build-upgrade-image
282+
cargo xtask run-tmt \
283+
--env=BOOTC_variant=composefs \
284+
--env=BOOTC_baseconfigs={{baseconfig}} \
285+
--upgrade-image={{upgrade_img}} \
286+
--composefs-backend \
287+
--bootloader={{bootloader}} \
288+
--filesystem={{filesystem}} \
289+
--boot-type={{boot_type}} \
290+
--seal-state={{seal_state}} \
291+
{{base_img}} readonly {{ARGS}}
292+
293+
# Run readonly tests for all standard baseconfigs
294+
[group('testing')]
295+
test-baseconfigs *ARGS:
296+
just test-tmt-baseconfig etc-transient {{ARGS}}
297+
just test-tmt-baseconfig root-transient {{ARGS}}
298+
just test-tmt-baseconfig var-volatile {{ARGS}}
270299

271300
# Run tmt tests on Fedora CoreOS
272301
[group('testing')]
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/bin/bash
2+
# Inject base configuration files for CI testing of transient-root/etc/var configurations.
3+
# Arguments: $1=variant, $2=baseconfigs (comma-separated, may be empty)
4+
set -xeuo pipefail
5+
6+
VARIANT="${1:-}"
7+
BASECONFIGS="${2:-}"
8+
9+
# No-op if no baseconfigs specified
10+
if [ -z "${BASECONFIGS}" ]; then
11+
exit 0
12+
fi
13+
14+
# setup-root-conf.toml is composefs-specific; ostree uses prepare-root.conf
15+
# which has a different (INI) format and different option names.
16+
case "${VARIANT}" in
17+
composefs*)
18+
TARGET="/usr/lib/composefs/setup-root-conf.toml"
19+
;;
20+
*)
21+
echo "inject-baseconfig: baseconfigs not supported for variant '${VARIANT}'" >&2
22+
exit 1
23+
;;
24+
esac
25+
26+
mkdir -p "$(dirname "${TARGET}")"
27+
28+
# Split on commas and process each token
29+
IFS=',' read -ra TOKENS <<< "${BASECONFIGS}"
30+
for raw_token in "${TOKENS[@]}"; do
31+
# Trim leading/trailing spaces
32+
token="${raw_token#"${raw_token%%[![:space:]]*}"}"
33+
token="${token%"${token##*[![:space:]]}"}"
34+
35+
[ -z "${token}" ] && continue
36+
37+
case "${token}" in
38+
etc-transient)
39+
printf '[etc]\ntransient = true\n' >> "${TARGET}"
40+
;;
41+
root-transient)
42+
printf '[root]\ntransient = true\n' >> "${TARGET}"
43+
;;
44+
var-volatile)
45+
# Mount /var as a fresh tmpfs on every boot via systemd.volatile=state.
46+
# bootc-root-setup detects this karg in the initramfs and automatically
47+
# skips the /var state bind-mount, leaving /var as an empty directory
48+
# from the composefs image. systemd-fstab-generator then mounts a fresh
49+
# tmpfs there at local-fs.target. Using a plain tmpfs avoids the
50+
# overlayfs-on-overlayfs restriction that breaks tools like podman which
51+
# use overlayfs under /var/lib/containers.
52+
mkdir -p /usr/lib/bootc/kargs.d
53+
printf 'kargs = ["systemd.volatile=state"]\n' \
54+
> /usr/lib/bootc/kargs.d/50-var-volatile.toml
55+
;;
56+
*)
57+
echo "Unknown baseconfig: ${token}" >&2
58+
exit 1
59+
;;
60+
esac
61+
done

crates/initramfs/src/lib.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ struct Config {
116116
#[serde(default)]
117117
etc: MountConfig,
118118
#[serde(default)]
119+
var: MountConfig,
120+
#[serde(default)]
119121
root: RootConfig,
120122
}
121123

@@ -437,6 +439,30 @@ pub fn setup_root(args: Args) -> Result<()> {
437439
.cmdline
438440
.unwrap_or(Cmdline::from_proc().context("Failed to read cmdline")?);
439441

442+
// Auto-detect systemd.volatile=state: if the kernel cmdline requests a
443+
// volatile /var via the systemd fstab-generator, skip our initramfs
444+
// bind-mount of /var from the deployment state directory. This leaves
445+
// /var as an empty directory from the composefs image so that
446+
// systemd-fstab-generator can mount a fresh tmpfs there at local-fs.target.
447+
// An explicit `[var] mount = "none"` in setup-root-conf.toml has the same
448+
// effect; the cmdline check is a convenience so users only need the kargs.d
449+
// entry without also editing setup-root-conf.toml.
450+
let config = {
451+
let mut config = config;
452+
// value_of returns None for a missing key, Some("") for a bare flag,
453+
// or Some("state") / Some("overlay") / Some("yes") for key=value form.
454+
let volatile_val = cmdline.value_of("systemd.volatile");
455+
let var_volatile = matches!(volatile_val, Some("state") | Some("overlay"));
456+
if var_volatile && config.var.mount.is_none() && !config.var.transient {
457+
tracing::debug!(
458+
"systemd.volatile={} detected; skipping /var state bind-mount",
459+
volatile_val.unwrap_or("")
460+
);
461+
config.var.mount = Some(MountType::None);
462+
}
463+
config
464+
};
465+
440466
let (image, insecure) = get_cmdline_composefs::<Sha512HashValue>(&cmdline)?;
441467

442468
let new_root = match &args.root_fs {
@@ -509,7 +535,12 @@ pub fn setup_root(args: Args) -> Result<()> {
509535
// etc + var
510536
let state = open_dir(open_dir(&sysroot, "state/deploy")?, image.to_hex())?;
511537
mount_subdir(visible_root, &state, "etc", config.etc, MountType::Bind)?;
512-
mount_subdir(visible_root, &state, "var", MountConfig::default(), MountType::Bind)?;
538+
// /var is bind-mounted from the deployment state directory by default.
539+
// The systemd.volatile=state cmdline detection above (or an explicit
540+
// [var] mount = "none" in setup-root-conf.toml) can change this to
541+
// MountType::None, which skips the bind-mount entirely and leaves /var
542+
// as an empty directory from the composefs image for systemd to fill.
543+
mount_subdir(visible_root, &state, "var", config.var, MountType::Bind)?;
513544

514545
if cfg!(not(feature = "pre-6.15")) {
515546
// Replace the /sysroot with the new composed root filesystem.
@@ -540,6 +571,10 @@ mod tests {
540571
mount: None,
541572
transient: false
542573
},
574+
var: MountConfig {
575+
mount: None,
576+
transient: false
577+
},
543578
root: RootConfig { transient: false },
544579
}
545580
);
@@ -564,6 +599,14 @@ mod tests {
564599
assert_eq!(config.etc.mount, None);
565600
}
566601

602+
#[test]
603+
fn test_var_none() {
604+
// mount = "none" skips the state bind-mount; combine with
605+
// systemd.volatile=state karg to get a fresh tmpfs on every boot.
606+
let config = parse("[var]\nmount = \"none\"");
607+
assert_eq!(config.var.mount, Some(MountType::None));
608+
}
609+
567610
#[test]
568611
fn test_root_transient() {
569612
let config = parse("[root]\ntransient = true");

docs/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
- [composefs backend](experimental-composefs.md)
6363
- [unified storage](experimental-unified-storage.md)
6464
- [`man bootc-root-setup.service`](man/bootc-root-setup.service.5.md)
65+
- [`man bootc-setup-root-conf.toml`](man/bootc-setup-root-conf.5.md)
6566
- [fsck](experimental-fsck.md)
6667
- [install reset](experimental-install-reset.md)
6768
- [--progress-fd](experimental-progress-fd.md)

0 commit comments

Comments
 (0)