Repository navigation
flavors via traits; Ubuntu+Rocky; k8s-CAPT image+in-memory; content-hashed caches in OCI; more powerful custom/ - #81
Open
rpardini wants to merge 14 commits into
Conversation
Copilot stopped reviewing on behalf of
jacobweinstock due to an error
November 3, 2026 16:43
rpardini
force-pushed
the
pr/flavors-via-traits-UbuntuRocky-k8s-CAPT-imagein-memory-content-hashed-caches-in-OCI-more-powerful-custom
branch
from
November 3, 2026 19:13
455c250 to
cc80a6c
Compare
rpardini
force-pushed
the
pr/flavors-via-traits-UbuntuRocky-k8s-CAPT-imagein-memory-content-hashed-caches-in-OCI-more-powerful-custom
branch
from
November 3, 2026 20:12
cc80a6c to
668f67c
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Registry passwords are logged in plaintext, permission-only file changes can leave cached images stale, and non-UEFI flavors are sent through an incompatible QEMU boot path.
4 open issues
3 resolved since last review
Review effort: Balanced
This reverts commit b73fb91.
- replace the BaseFlavor class chain and per-flavor modules with `Flavor` composed of traits, plus a registry in `captain/flavors.py` - traits live in `captain/traits/` (core, distro, platform, role, output); content collected along each trait's MRO - render one mkosi drop-in dir per trait content into disposable `mkosi.generated/`; mkosi runs with `--directory` - split `mkosi.conf.j2`, postinst and finalize per trait; custom/ becomes the last drop-in (`90-custom`) - builder container mounts `mkosi.generated/` instead of six generated paths - keep symlinks from trait content dirs (initrd*/systemd-firstboot masks, default.target) - no behaviour change: mkosi summary, merged trees and script bodies identical for all existing flavors Assisted-by: Claude Opus 5.5 Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
- `RpmFamily` distro trait: deb->rpm package-name map, systemd-networkd, update-ca-trust, SELinux off - `Rocky` distro trait: `Repositories=epel`, rocky-release/epel-release, `kernel` + `kernel-modules-extra` (EL10 moved nft_compat/xt_* there; CNI and kube-proxy need them) - `Ubuntu` distro trait (deb family): `Repositories=universe`, linux-image-generic; empty linux-firmware-minimal satisfies linux-firmware (initramfs 855M -> 206M) - flavors rockylinux10-full, resolute-full and resolute-rockchip64 - builder: add dnf, rpm, distribution-gpg-keys for mkosi RPM builds - /init: set PATH explicitly (Rocky's bash default PATH lacks sbin) - mem output: per-distro DTB location and switch_root provider; export DTBs before dropping /boot; os-release ID_LIKE per distro - DTB location comes from the platform (kernel provider); Armbian keeps Debian layout on any distro - armbian-next: add armbian-<release> suite only for releases that have one - tink role: install tar and gzip (tink-agent-setup needs them; not Essential on Rocky) - bash header exports CAPTAIN_FLAVOR_ID / CAPTAIN_DISTRO* for scripts, incl. custom/ ones; example postinst handles rpm CA store Assisted-by: Claude Opus 5.5 Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
- `Kubernetes`/`KubernetesCapt` role traits: pkgs.k8s.io repo (apt+dnf), kubelet/kubeadm/kubectl/cri-tools, cloud-init
- containerd config with CRI and SystemdCgroup, crictl config, br_netfilter/overlay, k8s sysctls, kubelet+cloud-init presets
- capt: cloud-init Ec2 datasource for Tinkerbell's Tootles (strict_id off, ds-identify enabled, 30s max wait)
- lsb-release on deb distros (cloud-init runs lsb_release); dropped on RPM ones (no redhat-lsb in EL10)
- `--kubernetes-version`/KUBERNETES_VERSION (default 1.36), passed into the builder container with ARMBIAN_VERSION
- flavors mem-k8s-capt-{trixie-full,resolute-full,rockylinux10-full,trixie-rockchip64}
- mem output keeps the kernel config as /usr/lib/modules/<kver>/config (kubeadm preflight)
- banner/static-network presets move from tink role to core; empty-rendered .j2 files are skipped
- qemu: --mem/--smp (QEMU_MEM/QEMU_SMP)
Assisted-by: Claude Opus 5.5
Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
…are banners
- `Img` output trait: mkosi Format=disk, own repart defs (ESP + root, or root only), SectorSize=512, Bootable=no
- img postinst: distro initrd (initramfs-tools / dracut --no-hostonly); systemd-boot + BLS entry (uefi) or extlinux.conf (DT)
- root=PARTLABEL=root, openssh-server + sudo + growpart, fresh ssh host keys per machine
- root autologin on consoles (tty1 and any serial port), like mem; root password stays locked
- output trait owns artifact collection and mkosi output dir/kind; out/captainos-<flavor>-<arch>.img.zst
- flavors img-k8s-capt-{trixie-full,resolute-full,rockylinux10-full,trixie-rockchip64}
- qemu: boot img flavors from a grown scratch copy (UEFI, virtio), cloud-init NoCloud seed via SMBIOS + local HTTP, ssh port forward
- keep kernel config as /usr/lib/modules/<kver>/config for every output (kubeadm preflight)
- builder: add systemd-repart
- banner/motd templates name role, output and flavor id; motd hints come from the role
- core owns /etc/issue{,.net}; /etc/os-release for every flavor from the distro's (NAME/PRETTY_NAME/VARIANT/IMAGE_ID/BUILD_ID)
- deb flavors keeping apt divert os-release/issue/issue.net so base-files upgrades don't undo them
Assisted-by: Claude Opus 5.5
Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
- add trixie-armbian-rpi to the k8s-capt bases (img- and mem-) - img: new "rpi" boot scheme; vfat boot partition with RPi firmware, kernel8.img, initrd, DTBs/overlays, generic config.txt/cmdline.txt (no U-Boot) - rpi: firmware via ExtraTrees for img; netboot artifacts (firmware-rpi/) only for mem - img cmdline: rootwait; serial as primary console (/dev/console), tty1 too - img cmdline: cgroup_enable=memory cgroup_memory=1, as the firmware prepends cgroup_disable=memory (kubeadm preflight) Assisted-by: Claude Opus 5.5 Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
- `captain list-flavors`: table, `--json`, `--github-output` (build + combined matrices), `--release` lines - ci.yml: prepare job emits matrices; build/publish-combined jobs use fromJSON (publish-combined needs prepare) - combined = both arches (incl. img flavors); ISO downloads only for flavors with an ISO - release.yml: pull and tag loop over the registry; per-flavor `release_tag_suffix` keeps existing tags stable - release: free runner disk space before pulling all flavors (GitHub-hosted runners only) - caches: save under the restore step's key; initramfs/iso cached only for selected flavors (Flavor.ci_cache) Assisted-by: Claude Opus 5.5 Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
- captain/cache: pure-Python OCI client; ORAS-style artifacts (one deterministic tar layer, modes kept)
- keys hash declared inputs only (no git sha/host/paths); doc-only changes hit
- cached pieces: tools, kernel, mkosi output (initramfs/image), iso; traits add inputs (eg. RPi firmware tag)
- two levels: downstream fork cache first, then tinkerbell/captain upstream
- scope auto-detected from GHA context (fork PRs read-only) or local git remotes
- local builds read-only unless --cache-push; --cache {on,read,off}; --cache-only
- .captain-cache-key markers replace "file exists" idempotency; mkosi cache hit skips tools and kernel stages
- emergency rebuilds: CACHE_EPOCH (global), per-distro cache_epoch
- `captain cache info` shows scope, keys and hits
- versions: resolve kernel/armbian tokens locally like CI's prepare job (cached 6h), so cache keys match
- builder: `--ensure-remote` checks own/upstream registries (manifest HEAD, no pull); obtain_builder falls back to upstream
- ci: registry-only handoff; drop actions/cache, workflow artifacts and the kernel warmer job; publish-combined restores with --cache-only
- ci: cache uv, CPython and wheels via local .github/actions/setup-uv; latest action versions
- drop ci_cache, --drop-old-caches, mkosi_image_name; mount Dockerfile into builder for kernel keys
Assisted-by: Claude Opus 5.5
Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
…on tags - publish under a content tag (c-<flavor>-<arch|combined>-<digest>) from new release keys; combined bases are the per-arch content tags - existing content tag: no build, no builder pull; always alias it to v0.0.0-<sha7>-<flavor>[-arch] - aliases and `release tag` are manifest re-tags via the Python OCI client (no blob transfer, no Docker) - new `captain release status [--github-output]`; ci skips building/restoring when already published - move builder image management to captain/builder.py (docker.py is in mkosi keys) - registry: existence checks accept any manifest type (registry:2 404s on indexes otherwise) - release.yml: also workflow_call (version, sha, prerelease, github-release); one flavor at a time - forks' main pushes get v0.0.0-ci.<run_number> OCI tags; GitHub prerelease only with repo var CAPTAIN_FORK_GH_RELEASE=true - `release tag`: single-arch flavors also get <tag>-<arch>; versions also tagged <version>-<flavor id>[-<arch>] - drop the now unused skopeo.copy Assisted-by: Claude Opus 5.5 Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
- progress: shared Rich transfer bar/spinner, terminal-only (no bars in CI/pipes) - bars on cache blob pull/push and tools/kernel downloads; spinners on pack/extract - cache: "not pushing (<reason>)" instead of "write to nowhere"; scope logged up front - cache: on first remote hit, say why it downloads instead of building - cli: --flavor alias for --flavor-id - rpi: drop duplicated firmware download log lines Assisted-by: Claude Opus 5.5 Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
…xt picks CI flavors - axes: family, distro, arch, role, output, platform, kernel, flavor; values from the traits (custom_names(), kernel_names()) - layers merge by name, general to specific: same-path files/same-named scripts replace, packages unioned - symlink a by-flavor dir to share it; warn on unmatched dirs and unknown axes - bash header: CAPTAIN_ARCH/ROLE/OUTPUT/PLATFORM/KERNEL for scripts to branch on - custom/builds.txt: flavor ids or globs CI builds (matrix, tags, releases); empty builds all; unmatched line fails - list-flavors table/json show a ci column when builds.txt narrows the selection - no-op examples: custom/by-flavor/README.md, custom/by-family/rpm/packages.txt, builds.txt listing every flavor Assisted-by: Claude Opus 5.5 Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
- README: flavors table (distros, k8s-capt mem/img, RPi), disk images, qemu, custom/ layers and builds.txt, build cache, release notes, Documentation section - design-decisions/flavor-traits.md: traits by axis, drop-ins, user customization layers, outputs and boot schemes (mermaid) - design-decisions/k8s-capt-image-builder-replace.md: img-k8s-capt flavors vs CAPT's image-builder - design-decisions/oci-tooling-buildah-skopeo.md: release content tags addendum - build-caching.md: keys, scope, CI graph, builder image, version tokens, release content tags, cold/cached results - fork-custom-builds.md: fork CI, image tags, consuming images, builds.txt, a repository holding only custom/ - gha-uv-python-deps-caching.md: uv/CPython/wheel caching in GHA - gotchas.md: mkosi, repart/disk images, distros, k8s/cloud-init, custom/, build tooling/CI, macOS - kernel-build.md: version token resolution Assisted-by: Claude Opus 5.5 Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
- dangling `default.target -> multi-user.target` broke review tools (ENOENT reading it from the repo) - drop `default.target`: core postinst already does `systemctl set-default multi-user.target` - mem: replace `initrd*` `/dev/null` symlinks with `systemctl mask` in a new mem postinst - core: drop `systemd-firstboot.service` mask; the unit is already removed outright - docs/gotchas: don't commit symlinks into trait dirs Assisted-by: Claude Opus 5.5 Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
- pass REGISTRY_USERNAME/PASSWORD to the builder as bare -e NAME (value from the env) - mask them in the DEBUG env table Assisted-by: Claude Opus 5.5 Signed-off-by: Ricardo Pardini <ricardo@pardini.net>
rpardini
force-pushed
the
pr/flavors-via-traits-UbuntuRocky-k8s-CAPT-imagein-memory-content-hashed-caches-in-OCI-more-powerful-custom
branch
from
November 3, 2026 21:06
668f67c to
a5dd68a
Compare
rpardini
marked this pull request as ready for review
November 3, 2026 22:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


flavors via traits; Ubuntu+Rocky; k8s-CAPT image+in-memory; content-hashed caches in OCI; more powerful
custom/Flavorcomposed of traits, plus a registry incaptain/flavors.pycaptain/traits/(core, distro, platform, role, output); content collected along each trait's MROmkosi.generated/; mkosi runs with--directorymkosi.conf.j2, postinst and finalize per trait; custom/ becomes the last drop-in (90-custom)mkosi.generated/instead of six generated pathsRpmFamilydistro trait: deb->rpm package-name map, systemd-networkd, update-ca-trust, SELinux offRockydistro trait:Repositories=epel, rocky-release/epel-release,kernel+kernel-modules-extra(EL10 moved nft_compat/xt_* there; CNI and kube-proxy need them)Ubuntudistro trait (deb family):Repositories=universe, linux-image-generic; empty linux-firmware-minimal satisfies linux-firmware (initramfs 855M -> 206M)Kubernetes/KubernetesCaptrole traits: pkgs.k8s.io repo (apt+dnf), kubelet/kubeadm/kubectl/cri-tools, cloud-init--kubernetes-version/KUBERNETES_VERSION (default 1.36), passed into the builder container with ARMBIAN_VERSIONImgoutput trait: mkosi Format=disk, own repart defs (ESP + root, or root only), SectorSize=512, Bootable=nocaptain list-flavors: table,--json,--github-output(build + combined matrices),--releaselinesrelease_tag_suffixkeeps existing tags stablecaptain cache infoshows scope, keys and hits--ensure-remotechecks own/upstream registries (manifest HEAD, no pull); obtain_builder falls back to upstreamrelease tagare manifest re-tags via the Python OCI client (no blob transfer, no Docker)captain release status [--github-output]; ci skips building/restoring when already publishedrelease tag: single-arch flavors also get -; versions also tagged -[-]default.target -> multi-user.targetbroke review tools (ENOENT reading it from the repo)default.target: core postinst already doessystemctl set-default multi-user.targetinitrd*/dev/nullsymlinks withsystemctl maskin a new mem postinstsystemd-firstboot.servicemask; the unit is already removed outright