Skip to content

Commit fe2e3cc

Browse files
committed
unified-storage: Implement three-store pipeline for composefs and ostree backends
The goal of unified storage is to make the same on-disk layer data simultaneously visible to containers-storage (for `podman run`), composefs (for the boot overlay), and ostree (for deployment tracking), using reflinks so layers are stored once regardless of how many stores reference them. This wires the full pipeline into the ostree backend. When a system has unified storage enabled — either at install time via the [install.storage] config key, or post-install via `bootc image set-unified` — upgrades and switches route through pull_via_composefs: Stage 1: pull image into bootc-owned containers-storage Stage 2: zero-copy reflink import into the composefs OCI repo Stage 3: synthesize an ostree commit from the composefs tree Whether unified storage is active is tracked by composefs/bootc.json (BootcRepoMeta). This replaces the previous heuristic that checked per-image presence in containers-storage, which broke when switching to a new image reference. The install config gains a storage.unified key with three values: disabled (default), enabled (fail if reflinks unavailable), and enabled-with-copy (copy fallback). This lets an image opt into unified storage without requiring a CLI flag to be threaded through every installer. bootc image list cross-references composefs tags against containers-storage by config digest to report images as unified (in all three stores) or partial (cstorage only, composefs import pending). bootc internals fsck images checks consistency and --repair restores cstorage from composefs when needed. The cstorage GC is extended to protect images that have composefs tags, since the composefs splitstreams reference the cstorage layer data — pruning one without the other would corrupt the repo. On the composefs-native backend, onboarding must also reconcile the non-booted deployments: a rollback or staged image lives in the composefs repo but not yet in containers-storage. `bootc image set-unified` and the new `bootc image sync` command now reconcile every bootloader-pinned deployment (booted, rollback, staged) into bootc's containers-storage, so `bootc internals fsck images` passes there too. The pinned set is grounded in the bootloader entries (via list_bootloader_entries), which are the authoritative record of what the system can boot, rather than the origin state directories which can drift from it. Reconciliation exports the missing image from the composefs repo. This must preserve the config digest, since that is the image ID containers-storage matches on: the export replays each layer's uncompressed tar byte-exactly via SplitStreamReader::cat() (preserving the diff_ids) and writes the original config JSON verbatim rather than re-serializing it. The recovered image is imported into bootc's private overlay store (not the host's default store) using the same bind-mount plumbing as the booted-image copy path, so fsck actually sees it. Assisted-by: OpenCode (claude-sonnet-4-6@default) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 068050a commit fe2e3cc

54 files changed

Lines changed: 4678 additions & 700 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ jobs:
158158
# These tests may mutate the system live so we can't run in parallel
159159
sudo bootc-integration-tests system-reinstall localhost/bootc --test-threads=1
160160
161+
# Unified storage case
162+
sudo podman build -t localhost/bootc-unified-storage -f ci/Containerfile.install-unified-storage
163+
sudo podman run --privileged --pid=host localhost/bootc-unified-storage bootc install to-existing-root --stateroot=unified-storage --acknowledge-destructive --skip-fetch-check
164+
# Verify unified storage was activated; composefs/bootc.json is written relative to
165+
# the target physical root (/target bind-mounted to host /), so the file appears at /composefs/bootc.json
166+
sudo test -f /composefs/bootc.json
167+
161168
# And the fsverity case
162169
sudo podman run --privileged --pid=host localhost/bootc-fsverity bootc install to-existing-root --stateroot=other \
163170
--acknowledge-destructive --skip-fetch-check

Justfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,12 +290,27 @@ test-tmt-baseconfig baseconfig *ARGS:
290290
--seal-state={{seal_state}} \
291291
{{base_img}} readonly {{ARGS}}
292292

293+
# Run unified-storage baseconfig test (works with ostree variant, no composefs required)
294+
[group('testing')]
295+
test-tmt-baseconfig-unified-storage *ARGS:
296+
just baseconfigs=unified-storage build
297+
just baseconfigs=unified-storage _build-upgrade-image
298+
cargo xtask run-tmt \
299+
--env=BOOTC_baseconfigs=unified-storage \
300+
--upgrade-image={{upgrade_img}} \
301+
--bootloader={{bootloader}} \
302+
--filesystem={{filesystem}} \
303+
--boot-type={{boot_type}} \
304+
--seal-state={{seal_state}} \
305+
{{base_img}} readonly {{ARGS}}
306+
293307
# Run readonly tests for all standard baseconfigs
294308
[group('testing')]
295309
test-baseconfigs *ARGS:
296310
just test-tmt-baseconfig etc-transient {{ARGS}}
297311
just test-tmt-baseconfig root-transient {{ARGS}}
298312
just test-tmt-baseconfig var-volatile {{ARGS}}
313+
just test-tmt-baseconfig-unified-storage {{ARGS}}
299314

300315
# Run tmt tests on Fedora CoreOS
301316
[group('testing')]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Enable unified storage (composefs+ostree) at install time via image-embedded config
2+
FROM localhost/bootc-install
3+
RUN <<EORUN
4+
set -xeuo pipefail
5+
mkdir -p /usr/lib/bootc/install
6+
printf '[install.storage]\nunified = "enabled-with-copy"\n' > /usr/lib/bootc/install/00-storage.toml
7+
bootc container lint
8+
EORUN

contrib/packaging/inject-baseconfig

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,8 @@ if [ -z "${BASECONFIGS}" ]; then
1111
exit 0
1212
fi
1313

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
14+
# Split and process tokens; unified-storage is handled before the variant check
15+
# because it is backend-independent (works with both ostree and composefs).
2916
IFS=',' read -ra TOKENS <<< "${BASECONFIGS}"
3017
for raw_token in "${TOKENS[@]}"; do
3118
# Trim leading/trailing spaces
@@ -35,27 +22,50 @@ for raw_token in "${TOKENS[@]}"; do
3522
[ -z "${token}" ] && continue
3623

3724
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
25+
unified-storage)
26+
# Write the bootc install config to enable unified storage at install time.
27+
# This is backend-independent and works with both ostree and composefs variants.
28+
mkdir -p /usr/lib/bootc/install
29+
printf '[install.storage]\nunified = "enabled-with-copy"\n' \
30+
> /usr/lib/bootc/install/00-storage.toml
5531
;;
5632
*)
57-
echo "Unknown baseconfig: ${token}" >&2
58-
exit 1
33+
# All other tokens require the composefs variant (they write to composefs-specific paths).
34+
# Validate variant here, not at the top, so unified-storage can run on any variant.
35+
case "${VARIANT}" in
36+
composefs*)
37+
TARGET="/usr/lib/composefs/setup-root-conf.toml"
38+
mkdir -p "$(dirname "${TARGET}")"
39+
;;
40+
*)
41+
echo "inject-baseconfig: baseconfig '${token}' not supported for variant '${VARIANT}'" >&2
42+
exit 1
43+
;;
44+
esac
45+
case "${token}" in
46+
etc-transient)
47+
printf '[etc]\ntransient = true\n' >> "${TARGET}"
48+
;;
49+
root-transient)
50+
printf '[root]\ntransient = true\n' >> "${TARGET}"
51+
;;
52+
var-volatile)
53+
# Mount /var as a fresh tmpfs on every boot via systemd.volatile=state.
54+
# bootc-root-setup detects this karg in the initramfs and automatically
55+
# skips the /var state bind-mount, leaving /var as an empty directory
56+
# from the composefs image. systemd-fstab-generator then mounts a fresh
57+
# tmpfs there at local-fs.target. Using a plain tmpfs avoids the
58+
# overlayfs-on-overlayfs restriction that breaks tools like podman which
59+
# use overlayfs under /var/lib/containers.
60+
mkdir -p /usr/lib/bootc/kargs.d
61+
printf 'kargs = ["systemd.volatile=state"]\n' \
62+
> /usr/lib/bootc/kargs.d/50-var-volatile.toml
63+
;;
64+
*)
65+
echo "Unknown baseconfig: ${token}" >&2
66+
exit 1
67+
;;
68+
esac
5969
;;
6070
esac
6171
done

0 commit comments

Comments
 (0)