Skip to content

docs(spec): Flatcar version parity on the FSDK base - #123

Open
castrojo wants to merge 12 commits into
mainfrom
docs/flatcar-base-migration-design
Open

castrojo wants to merge 12 commits into
mainfrom
docs/flatcar-base-migration-design

Conversation

@castrojo

@castrojo castrojo commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Design for version parity with Flatcar Container Linux: for each component Flatcar ships, build that same upstream version from source in BuildStream, on the freedesktop-sdk 26.08 base.

The question the project answers: what would Flatcar look like if it were built with BuildStream?

Version parity, not ABI parity

  • Version parity — systemd 257 means systemd 257; kernel 6.12.102 means 6.12.102.
  • ABI parity is explicitly not the goal. Flatcar's kernel banner reads x86_64-cros-linux-gnu-gcc (Gentoo Hardened 14.3.1_p20250801 p4). Our binaries will differ. Accepted.

Done is not "our bytes match theirs". Done is "we build the versions Flatcar builds, the result boots and behaves, and we can say precisely where a BuildStream-built Flatcar diverges from the real one."

Hard rule 1 is unchanged. The base stays FSDK 26.08. Flatcar binaries are imported as a reference to boot against and measure, never as the shipped payload.

Versioning

26.08.XX.$FLATCARVERSION      e.g. 26.08.13.4593.2.5

Both halves enforced: FSDK point release against the junction ref, Flatcar release against the pin in include/flatcar.yml.

Method: import first, then substitute

Parity needs an oracle. Import Flatcar's binaries, boot them on our installer and layout to establish a known-good control, then replace components one at a time with FSDK-built ones at Flatcar's versions, re-running the boot proof after each swap.

Boot proof (already achieved)

Built a conforming disk from upstream artifacts alone, unprivileged — sfdisk, mksquashfs, mke2fs -d, no root, no loop mounts — and booted the pinned kernel with no external initrd:

[    1.122448] Run /init as init process
Mounting /usr from /dev/vda2
[    1.776610] systemd[1]: Successfully made /usr/ read-only.
[    4.133550] systemd[1]: Switching root.
Welcome to Flatcar Container Linux by Kinvolk 4593.2.5 (Oklo)!
[  OK  ] Reached target multi-user.target - Multi-User System.
localhost login:

Findings that drove the design

  • CONFIG_INITRAMFS_SOURCE="bootengine.cpio" — the kernel already carries a two-stage initramfs. The installer has been generating a second one to lay over a working one.
  • veritysetup --hash-offset=1065345024 is hardcoded in stage 1: verity is one partition with the hash appended, not systemd-repart's two-partition model. /usr has a hard 1,065,345,024-byte budget.
  • Stage 2 is bootengine.img, loop-mounted from the mounted /usr — so it versions with the payload, and must survive the import.
  • OEM is required, matched by filesystem label; without it the boot stalls 90s then drops to emergency.
  • Ignition needs no masking — it degrades to Subsequent (Not Ignition) boot complete.
  • A/B is USR-A/USR-B (5dfbf5f4-2848-4bac-aa5e-0d9a20b745a6), not root-a/root-b.

Scope

Design only, no element changes. Ticket chain: #125 versioning · #137 pins · #142 parity matrix · #126 reference import · #134 layout · #135 verity · #128 dracut removal · #129 comparison harness · #130 boot proof in CI · #138 creds · #139 reboot coordination.

Assisted-by: GPT-5.6 Sol via GitHub Copilot

castrojo and others added 2 commits September 13, 2026 18:44
Bluefin Server composes userspace from FSDK 26.08 while taking its kernel
and ZFS from Flatcar 4593.2.5. Every installer bring-up defect came from
that seam: dracut built the target initrd against a foreign, nested module
tree, storage drivers had to be force-loaded by hand, and
os-release-flatcar.bst claims ID=flatcar purely so Flatcar sysexts attach.

Record the design that collapses the OS payload to a single ABI domain by
importing Flatcar's /usr as a pinned binary, keeping BuildStream as the only
build system and leaving the installer on FSDK, since systemd-sysinstall is
a systemd 261 tool with no Flatcar equivalent.

Assisted-by: GPT-5.6 Sol via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
flatcar_production_pxe_image.cpio.gz is not a driver initrd. Measured: four
cpio entries wrapping a 374 MiB usr.squashfs, i.e. the whole OS for a RAM
boot. Flatcar's real initramfs is /usr/lib/flatcar/bootengine.img, a 50 MiB
squashfs with /init and /etc/initrd-release built by sys-kernel/bootengine
0.0.38-r40, shipped inside /usr and versioned with the release.

Record the Ignition constraint that comes with it: bootengine drives
ignition-fetch/disks/mount/files and sysroot-boot, which this design rejects,
so those units must be masked or the fallback is mkosi-initrd --generic.

Assisted-by: GPT-5.6 Sol via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
castrojo and others added 4 commits September 13, 2026 18:57
flatcar_production_image_kernel_config.txt declares
CONFIG_INITRAMFS_SOURCE="bootengine.cpio" with XZ compression, so the kernel
flatcar-kernel.bst already imports is self-sufficient. Flatcar publishes a
manifest per stage: 339 entries for the busybox/veritysetup shim that sets up
dm-verity for /usr, 2,280 for the systemd initrd it pivots into, matching
bootengine.img exactly. flatcar_production_image.vmlinuz and
flatcar_production_pxe.vmlinuz are the same 34,245,760 bytes.

The installer has therefore been generating a second initrd to lay over a
working one. Deleting the dracut step needs no replacement artifact, only
conformance to the cmdline that initramfs expects: root=LABEL=ROOT with
usr=PARTLABEL=USR-A. That promotes dm-verity A/B from deferred follow-on to
the leading option, since it is the only layout upstream tests.

Assisted-by: GPT-5.6 Sol via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Booting the pinned flatcar_production_image.vmlinuz in QEMU with no -initrd
and no disk reaches "Run /init as init process" at 1.05s, then blocks on
"Waiting for drive..." while stage 1 hunts for usr=PARTLABEL=USR-A. That
demonstrates the initramfs is compiled in and live rather than merely
declared, and shows the boot contract asserting itself.

The same trace also shows the kernel prepends a built-in CONFIG_CMDLINE of
"rootflags=rw mount.usrflags=ro", so a UKI cmdline merges with it rather than
replacing it.

Assisted-by: GPT-5.6 Sol via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Decision: follow Flatcar's design. The OS payload becomes a /usr image on a
verity-protected A/B pair and / becomes writable state. Overriding the
built-in initramfs, by dracut or mkosi-initrd, is rejected.

Extracted stage 1's /init from the kernel's embedded initramfs and read the
contract instead of guessing it. veritysetup runs with a hardcoded
--hash-offset=1065345024, so verity is one partition with the hash tree
appended, not systemd-repart's two-partition model; the image carries its own
hash and repart keeps using CopyBlocks. Verity is skipped when
verity.usrhash is empty, so layout and verity split into separate tickets.
mount -t passes usrfstype through, so /usr stays XFS. Stage 1 loop-mounts
/usr/lib/flatcar/bootengine.img from the mounted /usr, so stage 2 ships with
the payload and must survive the import strip list.

Records Flatcar's GPT read from flatcar_production_image.bin and maps it onto
repart.d: EFI-SYSTEM, USR-A, USR-B, ROOT, with BIOS-BOOT, OEM, and OEM-CONFIG
dropped as BIOS-only or Ignition-only.

Assisted-by: GPT-5.6 Sol via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Built a conforming disk from upstream artifacts alone, unprivileged: GPT with
Flatcar's type GUIDs, /usr as a 356 MiB squashfs in USR-A, ext4 OEM and ROOT
via mke2fs -d. The pinned kernel booted it with no external initrd through
stage 1, bootengine, switch_root, and on to multi-user.target and a login
prompt, with SSH host keys and DHCP up.

Two corrections from the trace. OEM is required, not droppable: stage 2 waits
on dev-disk-by-label-OEM.device, matched by filesystem label rather than
partition label, and drops to an emergency shell after 90s without it.
Ignition needs no masking: ignition-setup-pre completes, delete-config skips
on its trigger condition, and ignition-subsequent.target reports "Not
Ignition" boot complete.

Also records the competing version-parity plan. Adopts its pin-discipline,
systemd-creds provisioning, and reboot coordination. Rejects rebuilding
Flatcar versions from source, since version parity is not ABI parity under a
different toolchain; rejects three tarball-built kernels, which would each
lose CONFIG_INITRAMFS_SOURCE and re-create the initrd problem; rejects
dropping k0s, which the KubeStellar path depends on; and supersedes its
root-a/root-b model with the measured USR-A/USR-B pair.

Assisted-by: GPT-5.6 Sol via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The objective is version parity, not ABI parity, and the question the project
answers is what Flatcar Container Linux looks like built with BuildStream.
For each component Flatcar ships, build that same upstream version from
source. Binaries will differ because the toolchain differs - Flatcar's kernel
banner reads Gentoo Hardened gcc 14.3.1 - and that is accepted. Success is
that the image boots and behaves and that every divergence is recorded, not
that bytes match.

The build base stays freedesktop-sdk 26.08 and hard rule 1 is unchanged.
Flatcar's binaries are imported as a reference to boot against and measure,
not as the shipped payload, so the method is import first then substitute,
with the boot proof as the control.

Release version becomes 26.08.XX.$FLATCARVERSION, one string with both halves
enforced: the FSDK point release against the junction ref, the Flatcar
release against the pin in include/flatcar.yml. This supersedes the earlier
two-field proposal.

Adds a version audit phase producing a parity matrix of component, Flatcar
version, FSDK version, gap, which orders all downstream work.

Assisted-by: GPT-5.6 Sol via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@castrojo castrojo changed the title docs(spec): plan migration of the OS payload to a Flatcar base docs(spec): Flatcar version parity on the FSDK base Sep 13, 2026
release-version becomes the Flatcar release the image targets, 4593.2.5, not
a composite and not a translation. If the image claims parity with Flatcar
4593.2.5 it says so.

The tree is already half-way there and inconsistent with itself:
os-release-flatcar.bst sets VERSION_ID and CPE_NAME from FLATCAR_VERSION
while PRETTY_NAME carries the unrelated release-version, so one image answers
two different questions about its own version. This collapses them.

It also makes sysext attachment honest. Flatcar sysexts match on ID and
VERSION_ID, and flatcar-zfs.raw attaches today only because the identity file
already asserts Flatcar's. With release-version equal to the Flatcar version
the match rests on a true statement.

check-release-version.py inverts: it stops enforcing against the FSDK
junction ref and starts enforcing against flatcar-version in
include/flatcar.yml. The FSDK pin stays for provenance in fsdk_ref and gets
its own os-release field rather than being smuggled into the version string.
Prerelease suffixes keep working, so an alpha is 4593.2.5-alpha.1.

Supersedes both the two-field split and the 26.08.XX.$FLATCARVERSION
composite.

Assisted-by: GPT-5.6 Sol via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@hanthor hanthor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This document contradicts the four implementation PRs open against it, and in places contradicts itself. As written it can't serve as the design of record for #132/#133/#136/#140/#141.

1. Hard rule 1: stated as unchanged, three times

The build base stays freedesktop-sdk 26.08. Hard rule 1 is unchanged: compose from FSDK components/*. Version parity is pursued within the FSDK graph, by building the versions Flatcar ships rather than by importing Flatcar's tree wholesale.

Both images are composed from FSDK 26.08. […] Installed OS payload tracks Flatcar's versions […] still built from FSDK components/*.

Hard rule 1 is untouched throughout: the base stays FSDK 26.08.

#133 changes that rule to "Installer image composes from FSDK 26.08 components/*; OS payload may import Flatcar binaries pinned by digest," and its ADR explicitly rejects the hybrid this document describes: "Keeping FSDK userspace in the OS payload while importing Flatcar kernel and ZFS artifacts preserves the ABI split that the migration is meant to remove."

2. Internally inconsistent

The paragraph above says "rather than by importing Flatcar's tree wholesale." Two sections later, ### New element: flatcar/flatcar-usr.bst specifies exactly that import, and ### Overlay: what Flatcar displaces tabulates the removal of the entire FSDK base runtime. Phase 3 then says the imports are "the oracle, not the payload" — but #132's os-stack.bst makes flatcar-usr.bst the payload.

These are two different designs in one file, and the reader can't tell which one governs.

3. ABI parity

This says "ABI parity is explicitly not the goal […] the binaries will differ, and that is expected and accepted." #132's element header says it "Replaces the FSDK 26.08 base userspace […] with a single-ABI Flatcar /usr." Opposite objectives.

4. §Versioning explicitly supersedes #136

.github/scripts/check-release-version.py inverts. It stops enforcing release-version against the FSDK junction ref and starts enforcing it against flatcar-version.

This supersedes both earlier proposals: the two-field split and the 26.08.XX.$FLATCARVERSION composite.

#136 is the two-field split (installer-version + flatcar-version) and it keeps enforcing installer-version against the junction ref. One of the two has to be withdrawn.

For what it's worth I think #136 has the better of the argument — this document's own "the installer and the payload diverge in version" section is an argument for two axes, since systemd-sysinstall only exists in the installer's FSDK systemd and the installer genuinely cannot follow the payload's version. But that reasoning supports two axes, not collapsing onto one.

Two things here the implementations should adopt, and one that's factually wrong

Adopt: "Removals are explicit rm lines with a comment naming the replacement, not a wildcard sweep, so a future Flatcar bump that renames a unit fails loudly." #140 follows this exactly. #132's single rm -rf block does not — and I confirmed against the real tarball that three of its paths don't exist and fail silently, while six real ones go unremoved.

Adopt: "Contract tests assert /usr/lib/flatcar/bootengine.img is present and the module layout is flat." #140 has these; #132 doesn't.

Wrong: the design (and both implementations) assume Flatcar nests modules at usr/lib/modules/<kver>/<kver>/. I listed all 19,658 entries of flatcar-container.tar.gz (sha256 c533ff57…, the ref both PRs pin) — the tree is flat. There is no nested directory to collapse, so the flattening block is dead code in both. The likely origin of the belief is the --strip-components=5 in the existing flatcar-kernel.bst.

Also missing

The "what Flatcar displaces" table maps podman → flatcar-podman.raw sysext, but #141 additionally imports containerd and docker, which this document doesn't mention at all. And the document says the target dracut invocation "is deleted, and nothing replaces it" — none of #132/#140/#141 touches elements/oci/bluefin-server-installer.bst, where that invocation lives.

docs-checks.py passes and pytest/bats are unaffected — this is a documentation-consistency objection, not a test failure.


Generated by Claude Code

@castrojo

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review. The core tensions you identified are valid and reflect the ongoing discussion around the migration strategy:

  1. Hard Rule 1 & Scoping: As established in PR docs(arch): scope FSDK hard rule to installer #133 and its ADR (docs/superpowers/specs/2026-09-13-flatcar-binary-imports-adr.md), the intended architectural boundary is scoping FSDK 26.08 composition strictly to the installer image (systemd-sysinstall), while permitting the installed OS payload to import digest-pinned Flatcar binaries (flatcar-usr.bst, sysexts). We will update this spec to align directly with docs(arch): scope FSDK hard rule to installer #133 rather than asserting that the OS payload rebuilds Flatcar components from source within FSDK.
  2. Version Parity vs. Single-ABI Import: The spec attempted to reconcile two competing frames ("build Flatcar versions in FSDK" vs "import Flatcar /usr as a single-ABI baseline"). We agree the hybrid framing introduces contradictions. The document will be revised so that the binary import model (single ABI domain, matching refactor: switch OS payload to Flatcar base, drop displaced FSDK components #132/feat(flatcar): add flatcar-usr.bst importing Flatcar /usr as the OS payload base #140) is clearly stated as the accepted design, resolving the conflict.
  3. Module nesting & dead code: Verified your finding on flatcar-container.tar.gz module layout being flat (no <kver>/<kver>/ nesting). The dead flattening logic and inaccurate nesting claims are being excised.
  4. Versioning Axes & refactor(arch): split release-version into installer and Flatcar axes #136: Acknowledged on the two-axis model (installer-version tracking FSDK and flatcar-version tracking the payload). We will align the versioning section with refactor(arch): split release-version into installer and Flatcar axes #136's two-axis approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants