diff --git a/elements/flatcar/flatcar-usr.bst b/elements/flatcar/flatcar-usr.bst new file mode 100644 index 0000000..ca23d98 --- /dev/null +++ b/elements/flatcar/flatcar-usr.bst @@ -0,0 +1,82 @@ +kind: manual +description: | + Import the Flatcar LTS /usr as the Bluefin Server OS payload base. + Extracts ./usr from upstream Flatcar container rootfs, strips Flatcar's + update and provisioning stack in favor of native Bluefin equivalents + (systemd-sysupdate, systemd-creds, Kured), and removes /usr/lib/modules + to leave kernel module management exclusively to flatcar-kernel.bst. + +(@): + - include/arch.yml + - include/flatcar.yml + +build-depends: + - base/base-stack.bst + - freedesktop-sdk.bst:components/tar.bst + - freedesktop-sdk.bst:components/gzip.bst + - freedesktop-sdk.bst:components/findutils.bst + +variables: + strip-binaries: "" + +sources: + - kind: remote + url: flatcar:stable/%{flatcar-board}/%{flatcar-version}/flatcar-container.tar.gz + ref: c533ff576e128d11ce240e01f818c358d75b9ff118b2dd9199fbf4684547a32c + +config: + install-commands: + - | + set -euo pipefail + + mkdir -p "%{install-root}" + + # Extract the complete Flatcar /usr into the element install root. + tar -xzf flatcar-container.tar.gz -C "%{install-root}" ./usr + + # Remove kernel modules; elements/flatcar/flatcar-kernel.bst extracts and owns + # /usr/lib/modules/%{flatcar-kver} (uncompressing .ko.xz modules and running depmod) + # to prevent filesystem overlap collisions when composed together in os-stack.bst. + rm -rf "%{install-root}/usr/lib/modules" + + # Remove Flatcar's update and provisioning stack with explicit rm lines. + # Each removal names its Bluefin replacement. No wildcard sweeps are used + # so that an upstream unit rename fails loudly instead of silently regressing. + # Note: /usr/lib/flatcar/bootengine.img is deliberately preserved here; + # stage 1 initramfs loop-mounts it from /usr as the stage 2 initramfs. + + # update_engine: replaced by Bluefin's systemd-sysupdate (os-sysupdate.bst) for A/B image updates + rm "%{install-root}/usr/bin/update_engine" + + # update_engine_client: replaced by Bluefin's systemd-sysupdate CLI and systemctl + rm "%{install-root}/usr/bin/update_engine_client" + + # update_engine_stub: replaced by Bluefin's native systemd-sysupdate mechanism + rm "%{install-root}/usr/bin/update_engine_stub" + + # locksmithd: replaced by Bluefin's Kured reboot coordination (os-kured-hook.bst) via /run/reboot-required + rm "%{install-root}/usr/lib/locksmith/locksmithd" + + # locksmithd.service: replaced by Bluefin's Kured reboot coordination (os-kured-hook.bst) + rm "%{install-root}/usr/lib/systemd/system/locksmithd.service" + rm "%{install-root}/usr/lib/systemd/system/multi-user.target.wants/locksmithd.service" + + # ignition: replaced by Bluefin's systemd-creds first-boot credential provisioning (os-creds-prov.bst) + rm "%{install-root}/usr/bin/ignition" + + # ignition-delete-config.service: replaced by Bluefin's systemd-creds credential provisioning + rm "%{install-root}/usr/lib/systemd/system/ignition-delete-config.service" + rm "%{install-root}/usr/lib/systemd/system/sysinit.target.wants/ignition-delete-config.service" + + # coreos-cloudinit: replaced by Bluefin's systemd-creds first-boot credential provisioning (os-creds-prov.bst) + rm "%{install-root}/usr/bin/coreos-cloudinit" + + # flatcar-update: replaced by Bluefin's systemd-sysupdate (os-sysupdate.bst) and os-justfile + rm "%{install-root}/usr/bin/flatcar-update" + + # download_sysext: replaced by Bluefin's declarative sysext delivery via systemd-sysext + rm "%{install-root}/usr/bin/download_sysext" + + # ensure-sysext.service: replaced by Bluefin's systemd-sysext.service and sysupdate integration + rm "%{install-root}/usr/lib/systemd/system/ensure-sysext.service" + rm "%{install-root}/usr/lib/systemd/system/sysinit.target.wants/ensure-sysext.service" diff --git a/tests/unit/test_flatcar_usr.py b/tests/unit/test_flatcar_usr.py new file mode 100644 index 0000000..ee35b39 --- /dev/null +++ b/tests/unit/test_flatcar_usr.py @@ -0,0 +1,248 @@ +"""Contract tests for elements/flatcar/flatcar-usr.bst. + +Verifies that the Flatcar /usr base import: +- Uses kind: manual with prebuilt binaries unstripped (strip-binaries: ""). +- Imports flatcar-container.tar.gz pinned by sha256 via the flatcar: alias. +- Removes /usr/lib/modules to avoid filesystem collisions with flatcar-kernel.bst. +- Explicitly removes Flatcar's update and provisioning stack (update_engine, + locksmithd, ignition, coreos-cloudinit, flatcar-update, download_sysext, + ensure-sysext) with individual rm commands documented with their Bluefin replacements. +- Preserves usr/lib/flatcar/bootengine.img for two-stage kernel boot. +- Preserves verified OS components (systemd 257, glibc, bash, sshd, crictl, etc.). +""" + +from __future__ import annotations + +import re +import subprocess +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[2] +FLATCAR_USR_ELEMENT = REPO_ROOT / "elements" / "flatcar" / "flatcar-usr.bst" +FLATCAR_YML = REPO_ROOT / "include" / "flatcar.yml" + +REMOVED_BINARIES = [ + "usr/bin/update_engine", + "usr/bin/update_engine_client", + "usr/bin/update_engine_stub", + "usr/lib/locksmith/locksmithd", + "usr/bin/ignition", + "usr/bin/coreos-cloudinit", + "usr/bin/flatcar-update", + "usr/bin/download_sysext", +] + +REMOVED_UNITS = [ + "usr/lib/systemd/system/locksmithd.service", + "usr/lib/systemd/system/ignition-delete-config.service", + "usr/lib/systemd/system/ensure-sysext.service", +] + +REMOVED_COMPONENTS = [ + "update_engine", + "update_engine_client", + "update_engine_stub", + "locksmithd", + "locksmithd.service", + "ignition", + "ignition-delete-config.service", + "coreos-cloudinit", + "flatcar-update", + "download_sysext", + "ensure-sysext.service", +] + + +def _load_element_data() -> dict: + content = FLATCAR_USR_ELEMENT.read_text(encoding="utf-8") + return yaml.safe_load(content) + + +def test_flatcar_usr_element_structure() -> None: + """Verify flatcar-usr.bst declares the expected BuildStream structure.""" + assert FLATCAR_USR_ELEMENT.is_file(), f"{FLATCAR_USR_ELEMENT} must exist" + data = _load_element_data() + + assert data.get("kind") == "manual", "flatcar-usr.bst must be kind: manual" + + # Must include arch.yml and flatcar.yml + includes = data.get("(@)", []) + assert "include/arch.yml" in includes + assert "include/flatcar.yml" in includes + + # Must declare build-depends for sandbox execution + build_deps = data.get("build-depends", []) + assert "base/base-stack.bst" in build_deps, "Must depend on base/base-stack.bst" + assert "freedesktop-sdk.bst:components/tar.bst" in build_deps + assert "freedesktop-sdk.bst:components/gzip.bst" in build_deps + assert "freedesktop-sdk.bst:components/findutils.bst" in build_deps + + # Prebuilt binaries must not be stripped + variables = data.get("variables", {}) + assert variables.get("strip-binaries") == "", ( + "variables.strip-binaries must be empty string to prevent stripping" + ) + + # Exactly one remote source for flatcar-container.tar.gz pinned by sha256 + sources = data.get("sources", []) + assert len(sources) == 1, "Must declare exactly one source" + src = sources[0] + assert src.get("kind") == "remote" + assert src.get("url") == "flatcar:stable/%{flatcar-board}/%{flatcar-version}/flatcar-container.tar.gz" + assert len(src.get("ref", "")) == 64, "Source ref must be a valid sha256 hash" + + +def test_flatcar_usr_explicit_removals_and_replacements() -> None: + """Verify explicit rm lines and Bluefin replacement documentation.""" + content = FLATCAR_USR_ELEMENT.read_text(encoding="utf-8") + + for component in REMOVED_COMPONENTS: + assert component in content, ( + f"flatcar-usr.bst must explicitly name {component} in its removal commands" + ) + + # Assert no wildcard sweeps in removal section + install_commands = _load_element_data().get("config", {}).get("install-commands", []) + script = "\n".join(install_commands) + for line in script.splitlines(): + clean = line.strip() + if clean.startswith("rm "): + assert "*" not in clean, ( + f"Wildcard removal forbidden to ensure rename failures: {line}" + ) + + # Assert each removed component is commented with replacement + for component in [ + "update_engine", + "locksmithd", + "ignition", + "coreos-cloudinit", + "flatcar-update", + "download_sysext", + "ensure-sysext", + ]: + pattern = rf"#\s*{component}.*?:.*?replaced by" + assert re.search(pattern, content, re.IGNORECASE), ( + f"flatcar-usr.bst must document replacement for {component}" + ) + + +def test_flatcar_usr_preserves_bootengine_img() -> None: + """Verify usr/lib/flatcar/bootengine.img is preserved and never deleted.""" + content = FLATCAR_USR_ELEMENT.read_text(encoding="utf-8") + + assert "bootengine.img" in content, ( + "flatcar-usr.bst must explicitly document preservation of bootengine.img" + ) + + install_commands = _load_element_data().get("config", {}).get("install-commands", []) + script = "\n".join(install_commands) + for line in script.splitlines(): + if "bootengine" in line: + assert not line.strip().startswith("rm"), ( + f"bootengine.img must not be removed: {line}" + ) + + +def test_flatcar_usr_removes_modules() -> None: + """Verify script logic removes /usr/lib/modules to avoid collision with flatcar-kernel.bst.""" + content = FLATCAR_USR_ELEMENT.read_text(encoding="utf-8") + assert 'rm -rf "%{install-root}/usr/lib/modules"' in content, ( + "Script must remove /usr/lib/modules to prevent collision with flatcar-kernel.bst" + ) + + +def test_flatcar_usr_contract_execution(tmp_path: Path) -> None: + """Simulate execution of flatcar-usr.bst install-commands against a staged /usr. + + Verifies: + 1. Removed binaries are absent. + 2. Removed units are absent. + 3. bootengine.img is preserved. + 4. Module directory /usr/lib/modules is removed to avoid collision with flatcar-kernel.bst. + 5. Retained files (bash, sshd, systemd, crictl) remain untouched. + """ + flatcar_yml_data = yaml.safe_load(FLATCAR_YML.read_text(encoding="utf-8")) + kver = flatcar_yml_data["variables"]["flatcar-kver"] + + install_root = tmp_path / "install-root" + usr = install_root / "usr" + + # Populate mock files that flatcar-container.tar.gz delivers + bin_dir = usr / "bin" + bin_dir.mkdir(parents=True) + for b in [ + "bash", "crictl", "sshd", "update_engine", "update_engine_client", + "update_engine_stub", "ignition", "coreos-cloudinit", "flatcar-update", + "download_sysext" + ]: + (bin_dir / b).write_text(f"mock-{b}", encoding="utf-8") + + locksmith_dir = usr / "lib" / "locksmith" + locksmith_dir.mkdir(parents=True) + (locksmith_dir / "locksmithd").write_text("mock-locksmithd", encoding="utf-8") + + systemd_dir = usr / "lib" / "systemd" / "system" + systemd_dir.mkdir(parents=True) + (systemd_dir / "locksmithd.service").write_text("mock-unit", encoding="utf-8") + (systemd_dir / "ignition-delete-config.service").write_text("mock-unit", encoding="utf-8") + (systemd_dir / "ensure-sysext.service").write_text("mock-unit", encoding="utf-8") + + wants_dir1 = systemd_dir / "multi-user.target.wants" + wants_dir1.mkdir(parents=True) + (wants_dir1 / "locksmithd.service").symlink_to("../locksmithd.service") + + wants_dir2 = systemd_dir / "sysinit.target.wants" + wants_dir2.mkdir(parents=True) + (wants_dir2 / "ignition-delete-config.service").symlink_to("../ignition-delete-config.service") + (wants_dir2 / "ensure-sysext.service").symlink_to("../ensure-sysext.service") + + flatcar_dir = usr / "lib" / "flatcar" + flatcar_dir.mkdir(parents=True) + bootengine = flatcar_dir / "bootengine.img" + bootengine.write_bytes(b"mock-bootengine-squashfs") + + # Module directory: usr/lib/modules// + mod_dir = usr / "lib" / "modules" / kver + (mod_dir / "kernel" / "drivers").mkdir(parents=True) + (mod_dir / "kernel" / "drivers" / "driver.ko").write_bytes(b"mock-driver") + (mod_dir / "modules.dep").write_text("mock modules.dep", encoding="utf-8") + + # Extract script from flatcar-usr.bst and adapt variables + install_commands = _load_element_data().get("config", {}).get("install-commands", []) + script = "\n".join(install_commands) + + # Replace BuildStream variables + script = script.replace("%{install-root}", str(install_root)) + script = script.replace("%{flatcar-kver}", kver) + + # In test environment, the mock files are already populated, so bypass tar extraction + script = re.sub(r"tar\s+-xzf\s+flatcar-container\.tar\.gz\s+-C\s+.*", "# tar extract bypassed", script) + + # Run the script in bash + result = subprocess.run(["bash", "-c", script], capture_output=True, text=True) + assert result.returncode == 0, f"Script failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}" + + # 1. Assert removed binaries are absent + for rel_path in REMOVED_BINARIES: + target = install_root / rel_path + assert not target.exists(), f"Removed binary {rel_path} must be absent from output" + + # 2. Assert removed units are absent + for rel_path in REMOVED_UNITS: + target = install_root / rel_path + assert not target.exists(), f"Removed unit {rel_path} must be absent from output" + + # 3. Assert module tree is completely removed to avoid collision with flatcar-kernel.bst + assert not (usr / "lib" / "modules").exists(), ( + "/usr/lib/modules must be removed to avoid collision with flatcar-kernel.bst" + ) + + # 4. Assert usr/lib/flatcar/bootengine.img exists in output + assert bootengine.is_file(), "usr/lib/flatcar/bootengine.img must exist in element output" + + # 5. Assert kept binaries remain + for kept in ["bash", "crictl", "sshd"]: + assert (bin_dir / kept).is_file(), f"Kept binary {kept} must remain in output"